Merge from vscode 79a1f5a5ca0c6c53db617aa1fa5a2396d2caebe2

This commit is contained in:
ADS Merger
2020-05-31 19:47:51 +00:00
parent 84492049e8
commit 28be33cfea
913 changed files with 28242 additions and 15549 deletions

View File

@@ -9,7 +9,7 @@ import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IViewDescriptorService, ViewContainerLocation } from 'vs/workbench/common/views';
import { GLOBAL_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { GLOBAL_ACTIVITY_ID, ACCOUNTS_ACTIIVTY_ID } from 'vs/workbench/common/activity';
export class ActivityService implements IActivityService {
@@ -35,6 +35,10 @@ export class ActivityService implements IActivityService {
return Disposable.None;
}
showAccountsActivity({ badge, clazz, priority }: IActivity): IDisposable {
return this.activityBarService.showActivity(ACCOUNTS_ACTIIVTY_ID, badge, clazz, priority);
}
showGlobalActivity({ badge, clazz, priority }: IActivity): IDisposable {
return this.activityBarService.showActivity(GLOBAL_ACTIVITY_ID, badge, clazz, priority);
}

View File

@@ -25,6 +25,11 @@ export interface IActivityService {
*/
showViewContainerActivity(viewContainerId: string, badge: IActivity): IDisposable;
/**
* Show accounts activity
*/
showAccountsActivity(activity: IActivity): IDisposable;
/**
* Show global activity
*/

View File

@@ -12,6 +12,9 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { MainThreadAuthenticationProvider } from 'vs/workbench/api/browser/mainThreadAuthentication';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IActivityService, NumberBadge } from 'vs/workbench/services/activity/common/activity';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
export const IAuthenticationService = createDecorator<IAuthenticationService>('IAuthenticationService');
@@ -19,24 +22,55 @@ export interface IAuthenticationService {
_serviceBrand: undefined;
isAuthenticationProviderRegistered(id: string): boolean;
getProviderIds(): string[];
registerAuthenticationProvider(id: string, provider: MainThreadAuthenticationProvider): void;
unregisterAuthenticationProvider(id: string): void;
requestNewSession(id: string, scopes: string[], extensionId: string, extensionName: string): void;
sessionsUpdate(providerId: string, event: AuthenticationSessionsChangeEvent): void;
readonly onDidRegisterAuthenticationProvider: Event<string>;
readonly onDidUnregisterAuthenticationProvider: Event<string>;
readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }>;
getSessions(providerId: string): Promise<ReadonlyArray<AuthenticationSession> | undefined>;
getSessions(providerId: string): Promise<ReadonlyArray<AuthenticationSession>>;
getDisplayName(providerId: string): string;
supportsMultipleAccounts(providerId: string): boolean;
login(providerId: string, scopes: string[]): Promise<AuthenticationSession>;
logout(providerId: string, accountId: string): Promise<void>;
logout(providerId: string, sessionId: string): Promise<void>;
}
export interface AllowedExtension {
id: string;
name: string;
}
export function readAllowedExtensions(storageService: IStorageService, providerId: string, accountName: string): AllowedExtension[] {
let trustedExtensions: AllowedExtension[] = [];
try {
const trustedExtensionSrc = storageService.get(`${providerId}-${accountName}`, StorageScope.GLOBAL);
if (trustedExtensionSrc) {
trustedExtensions = JSON.parse(trustedExtensionSrc);
}
} catch (err) { }
return trustedExtensions;
}
export interface SessionRequest {
disposables: IDisposable[];
requestingExtensionIds: string[];
}
export interface SessionRequestInfo {
[scopes: string]: SessionRequest;
}
export class AuthenticationService extends Disposable implements IAuthenticationService {
_serviceBrand: undefined;
private _placeholderMenuItem: IDisposable | undefined;
private _noAccountsMenuItem: IDisposable | undefined;
private _signInRequestItems = new Map<string, SessionRequestInfo>();
private _badgeDisposable: IDisposable | undefined;
private _authenticationProviders: Map<string, MainThreadAuthenticationProvider> = new Map<string, MainThreadAuthenticationProvider>();
@@ -49,7 +83,7 @@ export class AuthenticationService extends Disposable implements IAuthentication
private _onDidChangeSessions: Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._register(new Emitter<{ providerId: string, event: AuthenticationSessionsChangeEvent }>());
readonly onDidChangeSessions: Event<{ providerId: string, event: AuthenticationSessionsChangeEvent }> = this._onDidChangeSessions.event;
constructor() {
constructor(@IActivityService private readonly activityService: IActivityService) {
super();
this._placeholderMenuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
command: {
@@ -60,6 +94,14 @@ export class AuthenticationService extends Disposable implements IAuthentication
});
}
getProviderIds(): string[] {
const providerIds: string[] = [];
this._authenticationProviders.forEach(provider => {
providerIds.push(provider.id);
});
return providerIds;
}
isAuthenticationProviderRegistered(id: string): boolean {
return this._authenticationProviders.has(id);
}
@@ -125,9 +167,125 @@ export class AuthenticationService extends Disposable implements IAuthentication
if (provider) {
await provider.updateSessionItems(event);
this.updateAccountsMenuItem();
if (event.added) {
await this.updateNewSessionRequests(provider);
}
}
}
private async updateNewSessionRequests(provider: MainThreadAuthenticationProvider): Promise<void> {
const existingRequestsForProvider = this._signInRequestItems.get(provider.id);
if (!existingRequestsForProvider) {
return;
}
const sessions = await provider.getSessions();
let changed = false;
Object.keys(existingRequestsForProvider).forEach(requestedScopes => {
if (sessions.some(session => session.scopes.sort().join('') === requestedScopes)) {
// Request has been completed
changed = true;
const sessionRequest = existingRequestsForProvider[requestedScopes];
sessionRequest?.disposables.forEach(item => item.dispose());
delete existingRequestsForProvider[requestedScopes];
if (Object.keys(existingRequestsForProvider).length === 0) {
this._signInRequestItems.delete(provider.id);
} else {
this._signInRequestItems.set(provider.id, existingRequestsForProvider);
}
}
});
if (changed) {
if (this._signInRequestItems.size === 0) {
this._badgeDisposable?.dispose();
this._badgeDisposable = undefined;
} else {
let numberOfRequests = 0;
this._signInRequestItems.forEach(providerRequests => {
Object.keys(providerRequests).forEach(request => {
numberOfRequests += providerRequests[request].requestingExtensionIds.length;
});
});
const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested"));
this._badgeDisposable = this.activityService.showAccountsActivity({ badge });
}
}
}
requestNewSession(providerId: string, scopes: string[], extensionId: string, extensionName: string): void {
const provider = this._authenticationProviders.get(providerId);
if (provider) {
const providerRequests = this._signInRequestItems.get(providerId);
const scopesList = scopes.sort().join('');
const extensionHasExistingRequest = providerRequests
&& providerRequests[scopesList]
&& providerRequests[scopesList].requestingExtensionIds.includes(extensionId);
if (extensionHasExistingRequest) {
return;
}
const menuItem = MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '2_signInRequests',
command: {
id: `${extensionId}signIn`,
title: nls.localize('signInRequest', "Sign in to use {0} (1)", extensionName)
}
});
const signInCommand = CommandsRegistry.registerCommand({
id: `${extensionId}signIn`,
handler: async (accessor) => {
const authenticationService = accessor.get(IAuthenticationService);
const storageService = accessor.get(IStorageService);
const session = await authenticationService.login(providerId, scopes);
// Add extension to allow list since user explicitly signed in on behalf of it
const allowList = readAllowedExtensions(storageService, providerId, session.account.displayName);
if (!allowList.find(allowed => allowed.id === extensionId)) {
allowList.push({ id: extensionId, name: extensionName });
storageService.store(`${providerId}-${session.account.displayName}`, JSON.stringify(allowList), StorageScope.GLOBAL);
}
// And also set it as the preferred account for the extension
storageService.store(`${extensionName}-${providerId}`, session.id, StorageScope.GLOBAL);
}
});
if (providerRequests) {
const existingRequest = providerRequests[scopesList] || { disposables: [], requestingExtensionIds: [] };
providerRequests[scopesList] = {
disposables: [...existingRequest.disposables, menuItem, signInCommand],
requestingExtensionIds: [...existingRequest.requestingExtensionIds, extensionId]
};
this._signInRequestItems.set(providerId, providerRequests);
} else {
this._signInRequestItems.set(providerId, {
[scopesList]: {
disposables: [menuItem, signInCommand],
requestingExtensionIds: [extensionId]
}
});
}
let numberOfRequests = 0;
this._signInRequestItems.forEach(providerRequests => {
Object.keys(providerRequests).forEach(request => {
numberOfRequests += providerRequests[request].requestingExtensionIds.length;
});
});
const badge = new NumberBadge(numberOfRequests, () => nls.localize('sign in', "Sign in requested"));
this._badgeDisposable = this.activityService.showAccountsActivity({ badge });
}
}
getDisplayName(id: string): string {
const authProvider = this._authenticationProviders.get(id);
if (authProvider) {
@@ -137,13 +295,22 @@ export class AuthenticationService extends Disposable implements IAuthentication
}
}
async getSessions(id: string): Promise<ReadonlyArray<AuthenticationSession> | undefined> {
supportsMultipleAccounts(id: string): boolean {
const authProvider = this._authenticationProviders.get(id);
if (authProvider) {
return authProvider.supportsMultipleAccounts;
} else {
throw new Error(`No authentication provider '${id}' is currently registered.`);
}
}
async getSessions(id: string): Promise<ReadonlyArray<AuthenticationSession>> {
const authProvider = this._authenticationProviders.get(id);
if (authProvider) {
return await authProvider.getSessions();
} else {
throw new Error(`No authentication provider '${id}' is currently registered.`);
}
return undefined;
}
async login(id: string, scopes: string[]): Promise<AuthenticationSession> {
@@ -155,10 +322,10 @@ export class AuthenticationService extends Disposable implements IAuthentication
}
}
async logout(id: string, accountId: string): Promise<void> {
async logout(id: string, sessionId: string): Promise<void> {
const authProvider = this._authenticationProviders.get(id);
if (authProvider) {
return authProvider.logout(accountId);
return authProvider.logout(sessionId);
} else {
throw new Error(`No authentication provider '${id}' is currently registered.`);
}

View File

@@ -15,6 +15,9 @@ export class AuthenticationTokenService extends Disposable implements IAuthentic
_serviceBrand: undefined;
private readonly channel: IChannel;
private _token: IUserDataSyncAuthToken | undefined;
get token(): IUserDataSyncAuthToken | undefined { return this._token; }
private _onDidChangeToken = this._register(new Emitter<IUserDataSyncAuthToken | undefined>());
readonly onDidChangeToken = this._onDidChangeToken.event;
@@ -29,11 +32,8 @@ export class AuthenticationTokenService extends Disposable implements IAuthentic
this._register(this.channel.listen<void[]>('onTokenFailed')(_ => this.sendTokenFailed()));
}
getToken(): Promise<IUserDataSyncAuthToken | undefined> {
return this.channel.call('getToken');
}
setToken(token: IUserDataSyncAuthToken | undefined): Promise<undefined> {
this._token = token;
return this.channel.call('setToken', token);
}

View File

@@ -322,15 +322,16 @@ class BackupFileServiceImpl extends Disposable implements IBackupFileService {
return coalesce(backups);
}
private async readToMatchingString(file: URI, matchingString: string, maximumBytesToRead: number): Promise<string> {
private async readToMatchingString(file: URI, matchingString: string, maximumBytesToRead: number): Promise<string | undefined> {
const contents = (await this.fileService.readFile(file, { length: maximumBytesToRead })).value.toString();
const newLineIndex = contents.indexOf(matchingString);
if (newLineIndex >= 0) {
return contents.substr(0, newLineIndex);
const matchingStringIndex = contents.indexOf(matchingString);
if (matchingStringIndex >= 0) {
return contents.substr(0, matchingStringIndex);
}
throw new Error(`Backup: Could not find ${JSON.stringify(matchingString)} in first ${maximumBytesToRead} bytes of ${file}`);
// Unable to find matching string in file
return undefined;
}
async resolve<T extends object>(resource: URI): Promise<IResolvedBackup<T> | undefined> {
@@ -387,7 +388,7 @@ class BackupFileServiceImpl extends Disposable implements IBackupFileService {
// the meta-end marker ('\n') and as such the backup can only be invalid. We bail out
// here if that is the case.
if (!metaEndFound) {
this.logService.error(`Backup: Could not find meta end marker in ${backupResource}. The file is probably corrupt.`);
this.logService.trace(`Backup: Could not find meta end marker in ${backupResource}. The file is probably corrupt (filesize: ${content.size}).`);
return undefined;
}

View File

@@ -8,6 +8,7 @@ import { clipboard } from 'electron';
import { URI } from 'vs/base/common/uri';
import { isMacintosh } from 'vs/base/common/platform';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class NativeClipboardService implements IClipboardService {
@@ -15,51 +16,52 @@ export class NativeClipboardService implements IClipboardService {
_serviceBrand: undefined;
constructor(
@IElectronService private readonly electronService: IElectronService
) { }
async writeText(text: string, type?: 'selection' | 'clipboard'): Promise<void> {
clipboard.writeText(text, type);
return this.electronService.writeClipboardText(text, type);
}
async readText(type?: 'selection' | 'clipboard'): Promise<string> {
return clipboard.readText(type);
return this.electronService.readClipboardText(type);
}
readTextSync(): string {
return clipboard.readText();
}
readFindText(): string {
async readFindText(): Promise<string> {
if (isMacintosh) {
return clipboard.readFindText();
return this.electronService.readClipboardFindText();
}
return '';
}
writeFindText(text: string): void {
async writeFindText(text: string): Promise<void> {
if (isMacintosh) {
clipboard.writeFindText(text);
return this.electronService.writeClipboardFindText(text);
}
}
writeResources(resources: URI[]): void {
async writeResources(resources: URI[]): Promise<void> {
if (resources.length) {
clipboard.writeBuffer(NativeClipboardService.FILE_FORMAT, this.resourcesToBuffer(resources));
return this.electronService.writeClipboardBuffer(NativeClipboardService.FILE_FORMAT, this.resourcesToBuffer(resources));
}
}
readResources(): URI[] {
return this.bufferToResources(clipboard.readBuffer(NativeClipboardService.FILE_FORMAT));
async readResources(): Promise<URI[]> {
return this.bufferToResources(await this.electronService.readClipboardBuffer(NativeClipboardService.FILE_FORMAT));
}
hasResources(): boolean {
return clipboard.has(NativeClipboardService.FILE_FORMAT);
async hasResources(): Promise<boolean> {
return this.electronService.hasClipboard(NativeClipboardService.FILE_FORMAT);
}
private resourcesToBuffer(resources: URI[]): Buffer {
return Buffer.from(resources.map(r => r.toString()).join('\n'));
}
private bufferToResources(buffer: Buffer): URI[] {
private bufferToResources(buffer: Uint8Array): URI[] {
if (!buffer) {
return [];
}
@@ -75,6 +77,27 @@ export class NativeClipboardService implements IClipboardService {
return []; // do not trust clipboard data
}
}
/** @deprecated */
readTextSync(): string {
return clipboard.readText();
}
/** @deprecated */
readFindTextSync(): string {
if (isMacintosh) {
return clipboard.readFindText();
}
return '';
}
/** @deprecated */
writeFindTextSync(text: string): void {
if (isMacintosh) {
clipboard.writeFindText(text);
}
}
}
registerSingleton(IClipboardService, NativeClipboardService, true);

View File

@@ -525,11 +525,13 @@ export class ConfigurationEditingService {
const model = reference.object.textEditorModel;
if (this.hasParseErrors(model, operation)) {
reference.dispose();
return this.reject<typeof reference>(ConfigurationEditingErrorCode.ERROR_INVALID_CONFIGURATION, target, operation);
}
// Target cannot be dirty if not writing into buffer
if (checkDirty && operation.resource && this.textFileService.isDirty(operation.resource)) {
reference.dispose();
return this.reject<typeof reference>(ConfigurationEditingErrorCode.ERROR_CONFIGURATION_FILE_DIRTY, target, operation);
}
return reference;
@@ -543,7 +545,7 @@ export class ConfigurationEditingService {
const standaloneConfigurationMap = target === EditableConfigurationTarget.USER_LOCAL ? USER_STANDALONE_CONFIGURATIONS : WORKSPACE_STANDALONE_CONFIGURATIONS;
const standaloneConfigurationKeys = Object.keys(standaloneConfigurationMap);
for (const key of standaloneConfigurationKeys) {
const resource = this.getConfigurationFileResource(target, config, standaloneConfigurationMap[key], overrides.resource);
const resource = this.getConfigurationFileResource(target, standaloneConfigurationMap[key], overrides.resource);
// Check for prefix
if (config.key === key) {
@@ -563,10 +565,10 @@ export class ConfigurationEditingService {
let key = config.key;
let jsonPath = overrides.overrideIdentifier ? [keyFromOverrideIdentifier(overrides.overrideIdentifier), key] : [key];
if (target === EditableConfigurationTarget.USER_LOCAL || target === EditableConfigurationTarget.USER_REMOTE) {
return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, config, '', null)), target };
return { key, jsonPath, value: config.value, resource: withNullAsUndefined(this.getConfigurationFileResource(target, '', null)), target };
}
const resource = this.getConfigurationFileResource(target, config, FOLDER_SETTINGS_PATH, overrides.resource);
const resource = this.getConfigurationFileResource(target, FOLDER_SETTINGS_PATH, overrides.resource);
if (this.isWorkspaceConfigurationResource(resource)) {
jsonPath = ['settings', ...jsonPath];
}
@@ -578,7 +580,7 @@ export class ConfigurationEditingService {
return !!(workspace.configuration && resource && workspace.configuration.fsPath === resource.fsPath);
}
private getConfigurationFileResource(target: EditableConfigurationTarget, config: IConfigurationValue, relativePath: string, resource: URI | null | undefined): URI | null {
private getConfigurationFileResource(target: EditableConfigurationTarget, relativePath: string, resource: URI | null | undefined): URI | null {
if (target === EditableConfigurationTarget.USER_LOCAL) {
if (relativePath) {
return resources.joinPath(resources.dirname(this.environmentService.settingsResource), relativePath);

View File

@@ -41,9 +41,11 @@ export class JSONEditingService implements IJSONEditingService {
private async doWriteConfiguration(resource: URI, values: IJSONValue[], save: boolean): Promise<void> {
const reference = await this.resolveAndValidate(resource, save);
await this.writeToBuffer(reference.object.textEditorModel, values, save);
reference.dispose();
try {
await this.writeToBuffer(reference.object.textEditorModel, values, save);
} finally {
reference.dispose();
}
}
private async writeToBuffer(model: ITextModel, values: IJSONValue[], save: boolean): Promise<any> {
@@ -108,11 +110,13 @@ export class JSONEditingService implements IJSONEditingService {
const model = reference.object.textEditorModel;
if (this.hasParseErrors(model)) {
reference.dispose();
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_INVALID_FILE);
}
// Target cannot be dirty if not writing into buffer
if (checkDirty && this.textFileService.isDirty(resource)) {
reference.dispose();
return this.reject<IReference<IResolvedTextEditorModel>>(JSONEditingErrorCode.ERROR_FILE_DIRTY);
}

View File

@@ -9,7 +9,7 @@ import * as dom from 'vs/base/browser/dom';
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { webFrame } from 'electron';
import { webFrame } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { unmnemonicLabel } from 'vs/base/common/labels';
import { Event, Emitter } from 'vs/base/common/event';
import { INotificationService } from 'vs/platform/notification/common/notification';
@@ -17,7 +17,7 @@ import { IContextMenuDelegate, ContextSubMenu, IContextMenuEvent } from 'vs/base
import { once } from 'vs/base/common/functional';
import { Disposable } from 'vs/base/common/lifecycle';
import { IContextMenuItem } from 'vs/base/parts/contextmenu/common/contextmenu';
import { popup } from 'vs/base/parts/contextmenu/electron-browser/contextmenu';
import { popup } from 'vs/base/parts/contextmenu/electron-sandbox/contextmenu';
import { getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { isMacintosh } from 'vs/base/common/platform';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';

View File

@@ -1,43 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { IdleValue } from 'vs/base/common/async';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
type KeytarModule = typeof import('keytar');
export class KeytarCredentialsService implements ICredentialsService {
_serviceBrand: undefined;
private readonly _keytar = new IdleValue<Promise<KeytarModule>>(() => import('keytar'));
async getPassword(service: string, account: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
return keytar.getPassword(service, account);
}
async setPassword(service: string, account: string, password: string): Promise<void> {
const keytar = await this._keytar.getValue();
return keytar.setPassword(service, account, password);
}
async deletePassword(service: string, account: string): Promise<boolean> {
const keytar = await this._keytar.getValue();
return keytar.deletePassword(service, account);
}
async findPassword(service: string): Promise<string | null> {
const keytar = await this._keytar.getValue();
return keytar.findPassword(service);
}
async findCredentials(service: string): Promise<Array<{ account: string, password: string }>> {
const keytar = await this._keytar.getValue();
return keytar.findCredentials(service);
}
}
registerSingleton(ICredentialsService, KeytarCredentialsService, true);

View File

@@ -279,21 +279,25 @@ export abstract class AbstractFileDialogService implements IFileDialogService {
return filter;
}));
// Filters are a bit weird on Windows, based on having a match or not:
// Match: we put the matching filter first so that it shows up selected and the all files last
// No match: we put the all files filter first
const allFilesFilter = { name: nls.localize('allFiles', "All Files"), extensions: ['*'] };
if (matchingFilter) {
filters.unshift(matchingFilter);
filters.unshift(allFilesFilter);
} else {
filters.unshift(allFilesFilter);
// We have no matching filter, e.g. because the language
// is unknown. We still add the extension to the list of
// filters though so that it can be picked
// (https://github.com/microsoft/vscode/issues/96283)
if (!matchingFilter && ext) {
matchingFilter = { name: trim(ext, '.').toUpperCase(), extensions: [trim(ext, '.')] };
}
// Allow to save file without extension
filters.push({ name: nls.localize('noExt', "No Extension"), extensions: [''] });
options.filters = filters;
// Order of filters is
// - File Extension Match
// - All Files
// - All Languages
// - No Extension
options.filters = coalesce([
matchingFilter,
{ name: nls.localize('allFiles', "All Files"), extensions: ['*'] },
...filters,
{ name: nls.localize('noExt', "No Extension"), extensions: [''] }
]);
return options;
}

View File

@@ -403,7 +403,7 @@ export class SimpleFileDialog {
this.filePickBox.validationMessage = undefined;
const filePickBoxUri = this.filePickBoxValue();
let updated: UpdateResult = UpdateResult.NotUpdated;
if (!resources.isEqual(this.currentFolder, filePickBoxUri, true)) {
if (!resources.extUriIgnorePathCase.isEqual(this.currentFolder, filePickBoxUri)) {
updated = await this.tryUpdateItems(value, filePickBoxUri);
}
if (updated === UpdateResult.NotUpdated) {
@@ -448,7 +448,7 @@ export class SimpleFileDialog {
private filePickBoxValue(): URI {
// The file pick box can't render everything, so we use the current folder to create the uri so that it is an existing path.
const directUri = this.remoteUriFrom(this.filePickBox.value);
const directUri = this.remoteUriFrom(this.filePickBox.value.trimRight());
const currentPath = this.pathFromUri(this.currentFolder);
if (equalsIgnoreCase(this.filePickBox.value, currentPath)) {
return this.currentFolder;
@@ -541,7 +541,7 @@ export class SimpleFileDialog {
value = this.pathFromUri(valueUri);
await this.updateItems(valueUri, true);
return UpdateResult.Updated;
} else if (!resources.isEqual(this.currentFolder, valueUri, true) && (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true)))) {
} else if (!resources.extUriIgnorePathCase.isEqual(this.currentFolder, valueUri) && (this.endsWithSlash(value) || (!resources.extUriIgnorePathCase.isEqual(this.currentFolder, resources.dirname(valueUri)) && resources.extUriIgnorePathCase.isEqualOrParent(this.currentFolder, resources.dirname(valueUri))))) {
let stat: IFileStat | undefined;
try {
stat = await this.fileService.resolve(valueUri);
@@ -560,7 +560,7 @@ export class SimpleFileDialog {
return UpdateResult.InvalidPath;
} else {
const inputUriDirname = resources.dirname(valueUri);
if (!resources.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), inputUriDirname, true)) {
if (!resources.extUriIgnorePathCase.isEqual(resources.removeTrailingPathSeparator(this.currentFolder), inputUriDirname)) {
let statWithoutTrailing: IFileStat | undefined;
try {
statWithoutTrailing = await this.fileService.resolve(inputUriDirname);
@@ -865,7 +865,7 @@ export class SimpleFileDialog {
private createBackItem(currFolder: URI): FileQuickPickItem | null {
const fileRepresentationCurr = this.currentFolder.with({ scheme: Schemas.file });
const fileRepresentationParent = resources.dirname(fileRepresentationCurr);
if (!resources.isEqual(fileRepresentationCurr, fileRepresentationParent, true)) {
if (!resources.extUriIgnorePathCase.isEqual(fileRepresentationCurr, fileRepresentationParent)) {
const parentFolder = resources.dirname(currFolder);
return { label: '..', uri: resources.addTrailingPathSeparator(parentFolder, this.separator), isFolder: true };
}

View File

@@ -21,8 +21,8 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IProductService } from 'vs/platform/product/common/productService';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { MessageBoxOptions } from 'electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { MessageBoxOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { fromNow } from 'vs/base/common/date';
interface IMassagedMessageBoxOptions {

View File

@@ -3,10 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { SaveDialogOptions, OpenDialogOptions } from 'electron';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { SaveDialogOptions, OpenDialogOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService, INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -16,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { AbstractFileDialogService } from 'vs/workbench/services/dialogs/browser/abstractFileDialogService';
import { Schemas } from 'vs/base/common/network';
import { IModeService } from 'vs/editor/common/services/modeService';

View File

@@ -15,9 +15,9 @@ import { IFileService, FileOperationEvent, FileOperation, FileChangesEvent, File
import { Schemas } from 'vs/base/common/network';
import { Event, Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { basename, isEqualOrParent, joinPath } from 'vs/base/common/resources';
import { basename, isEqualOrParent, joinPath, isEqual } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IResourceEditorInputType, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions, IOpenEditorOverrideEntry, ICustomEditorViewTypesHandler, ICustomEditorInfo } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable, IDisposable, dispose, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
@@ -36,6 +36,8 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { indexOfPath } from 'vs/base/common/extpath';
import { DEFAULT_CUSTOM_EDITOR, updateViewTypeSchema, editorAssociationsConfigurationNode } from 'vs/workbench/services/editor/common/editorAssociationsSetting';
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
type CachedEditorInput = ResourceEditorInput | IFileEditorInput | UntitledTextEditorInput;
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
@@ -63,6 +65,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
//#endregion
private readonly fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
@@ -70,7 +74,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
@ILabelService private readonly labelService: ILabelService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IUriIdentityService private readonly uriIdentitiyService: IUriIdentityService
) {
super();
@@ -295,7 +301,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
private closeOnFileDelete: boolean = false;
private fileEditorInputFactory = Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories).getFileEditorInputFactory();
private onConfigurationUpdated(configuration: IWorkbenchEditorConfiguration): void {
if (typeof configuration.workbench?.editor?.closeOnFileDelete === 'boolean') {
this.closeOnFileDelete = configuration.workbench.editor.closeOnFileDelete;
@@ -499,7 +505,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
for (const handler of this.openEditorHandlers) {
const result = handler.open(event.editor, event.options, group);
const result = handler.open(event.editor, event.options, group, event.context || OpenEditorContext.NEW_EDITOR);
const override = result?.override;
if (override) {
event.prevent((() => override.then(editor => withNullAsUndefined(editor))));
@@ -863,20 +869,25 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Resource Editor Support
const resourceEditorInput = input as IResourceEditorInput;
if (resourceEditorInput.resource instanceof URI) {
let label = resourceEditorInput.label;
if (!label) {
label = basename(resourceEditorInput.resource); // derive the label from the path
}
return this.createOrGetCached(resourceEditorInput.resource, () => {
// We do not trust the resource that is being passed in as being the truth
// (e.g. in terms of path casing) and as such we ask the URI service to give
// us the canconical form of the URI. As such we ensure that any editor that
// is being opened will use the same canonical form of the URI.
const canonicalResource = this.uriIdentitiyService.asCanonicalUri(resourceEditorInput.resource);
// Derive the label from the path if not provided explicitly
const label = resourceEditorInput.label || basename(canonicalResource);
return this.createOrGetCached(canonicalResource, () => {
// File
if (resourceEditorInput.forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(resourceEditorInput.resource)) {
return this.fileEditorInputFactory.createFileEditorInput(resourceEditorInput.resource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService);
if (resourceEditorInput.forceFile /* fix for https://github.com/Microsoft/vscode/issues/48275 */ || this.fileService.canHandleResource(canonicalResource)) {
return this.fileEditorInputFactory.createFileEditorInput(canonicalResource, resourceEditorInput.encoding, resourceEditorInput.mode, this.instantiationService);
}
// Resource
return this.instantiationService.createInstance(ResourceEditorInput, resourceEditorInput.label, resourceEditorInput.description, resourceEditorInput.resource, resourceEditorInput.mode);
return this.instantiationService.createInstance(ResourceEditorInput, resourceEditorInput.label, resourceEditorInput.description, canonicalResource, resourceEditorInput.mode);
}, cachedInput => {
// Untitled
@@ -1107,7 +1118,9 @@ export class EditorService extends Disposable implements EditorServiceImpl {
//#endregion
//#region Custom View Type
private customEditorViewTypesHandlers = new Map<string, ICustomEditorViewTypesHandler>();
private readonly customEditorViewTypesHandlers = new Map<string, ICustomEditorViewTypesHandler>();
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable {
if (this.customEditorViewTypesHandlers.has(source)) {
throw new Error(`Use a different name for the custom editor component, ${source} is already occupied.`);
@@ -1148,6 +1161,67 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
//#endregion
//#region Editor Tracking
whenClosed(resources: URI[], options?: { waitForSaved: boolean }): Promise<void> {
let remainingResources = [...resources];
return new Promise(resolve => {
const listener = this.onDidCloseEditor(async event => {
const detailsResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.DETAILS });
const masterResource = toResource(event.editor, { supportSideBySide: SideBySideEditor.MASTER });
// Remove from resources to wait for being closed based on the
// resources from editors that got closed
remainingResources = remainingResources.filter(resource => {
if (isEqual(resource, masterResource) || isEqual(resource, detailsResource)) {
return false; // remove - the closing editor matches this resource
}
return true; // keep - not yet closed
});
// All resources to wait for being closed are closed
if (remainingResources.length === 0) {
if (options?.waitForSaved) {
// If auto save is configured with the default delay (1s) it is possible
// to close the editor while the save still continues in the background. As such
// we have to also check if the files to track for are dirty and if so wait
// for them to get saved.
const dirtyFiles = resources.filter(resource => this.workingCopyService.isDirty(resource));
if (dirtyFiles.length > 0) {
await Promise.all(dirtyFiles.map(async dirtyFile => await this.whenSaved(dirtyFile)));
}
}
listener.dispose();
resolve();
}
});
});
}
private whenSaved(resource: URI): Promise<void> {
return new Promise(resolve => {
if (!this.workingCopyService.isDirty(resource)) {
return resolve(); // return early if resource is not dirty
}
// Otherwise resolve promise when resource is saved
const listener = this.workingCopyService.onDidChangeDirty(workingCopy => {
if (!workingCopy.isDirty() && isEqual(resource, workingCopy.resource)) {
listener.dispose();
resolve();
}
});
});
}
//#endregion
dispose(): void {
super.dispose();
@@ -1214,6 +1288,7 @@ export class DelegatingEditorService implements IEditorService {
get onDidActiveEditorChange(): Event<void> { return this.editorService.onDidActiveEditorChange; }
get onDidVisibleEditorsChange(): Event<void> { return this.editorService.onDidVisibleEditorsChange; }
get onDidCloseEditor(): Event<IEditorCloseEvent> { return this.editorService.onDidCloseEditor; }
get activeEditor(): IEditorInput | undefined { return this.editorService.activeEditor; }
get activeEditorPane(): IVisibleEditorPane | undefined { return this.editorService.activeEditorPane; }
@@ -1263,9 +1338,9 @@ export class DelegatingEditorService implements IEditorService {
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> { return this.editorService.revert(editors, options); }
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> { return this.editorService.revertAll(options); }
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable {
throw new Error('Method not implemented.');
}
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable { return this.editorService.registerCustomEditorViewTypesHandler(source, handler); }
whenClosed(resources: URI[]): Promise<void> { return this.editorService.whenClosed(resources); }
//#endregion
}

View File

@@ -359,6 +359,7 @@ export const enum GroupChangeKind {
EDITOR_ACTIVE,
EDITOR_LABEL,
EDITOR_PIN,
EDITOR_STICKY,
EDITOR_DIRTY
}
@@ -368,6 +369,12 @@ export interface IGroupChangeEvent {
editorIndex?: number;
}
export const enum OpenEditorContext {
NEW_EDITOR = 1,
MOVE_EDITOR = 2,
COPY_EDITOR = 3
}
export interface IEditorGroup {
/**
@@ -462,7 +469,7 @@ export interface IEditorGroup {
* @returns a promise that resolves around an IEditor instance unless
* the call failed, or the editor was not opened as active editor.
*/
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions): Promise<IEditorPane | null>;
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, context?: OpenEditorContext): Promise<IEditorPane | null>;
/**
* Opens editors in this group.

View File

@@ -5,10 +5,10 @@
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IResourceEditorInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextEditorPane, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane } from 'vs/workbench/common/editor';
import { IEditorInput, IEditorPane, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceEditorInput, IResourceDiffEditorInput, ITextEditorPane, ITextDiffEditorPane, IEditorIdentifier, ISaveOptions, IRevertOptions, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent } from 'vs/workbench/common/editor';
import { Event } from 'vs/base/common/event';
import { IEditor, IDiffEditor } from 'vs/editor/common/editorCommon';
import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IEditorGroup, IEditorReplacement, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
@@ -35,7 +35,7 @@ export interface IOpenEditorOverrideEntry {
}
export interface IOpenEditorOverrideHandler {
open(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, id?: string): IOpenEditorOverride | undefined;
open(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup, context: OpenEditorContext, id?: string): IOpenEditorOverride | undefined;
getEditorOverrides?(resource: URI, options: IEditorOptions | undefined, group: IEditorGroup | undefined): IOpenEditorOverrideEntry[];
}
@@ -103,6 +103,11 @@ export interface IEditorService {
*/
readonly onDidVisibleEditorsChange: Event<void>;
/**
* Emitted when an editor is closed.
*/
readonly onDidCloseEditor: Event<IEditorCloseEvent>;
/**
* The currently active editor pane or `undefined` if none. The editor pane is
* the workbench container for editors of any kind.
@@ -240,6 +245,9 @@ export interface IEditorService {
*/
overrideOpenEditor(handler: IOpenEditorOverrideHandler): IDisposable;
/**
* Register handlers for custom editor view types.
*/
registerCustomEditorViewTypesHandler(source: string, handler: ICustomEditorViewTypesHandler): IDisposable;
/**
@@ -279,4 +287,14 @@ export interface IEditorService {
* @returns `true` if all editors reverted and `false` otherwise.
*/
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean>;
/**
* Track the provided list of resources for being opened as editors
* and resolve once all have been closed.
*
* @param options use `waitForSaved: true` to wait for the resources
* being saved. If auto-save is enabled, it may be possible to close
* an editor while the save continues in the background.
*/
whenClosed(resources: URI[], options?: { waitForSaved: boolean }): Promise<void>;
}

View File

@@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Event } from 'vs/base/common/event';
import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, TestEditorPart, ITestInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation } from 'vs/workbench/services/editor/common/editorGroupsService';
import { GroupDirection, GroupsOrder, MergeGroupMode, GroupOrientation, GroupChangeKind, GroupLocation, OpenEditorContext } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { EditorOptions, CloseDirection, IEditorPartOptions, EditorsOrder } from 'vs/workbench/common/editor';
import { URI } from 'vs/base/common/uri';
@@ -369,8 +370,9 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
let activeEditorChangeCounter = 0;
let editorDidOpenCounter = 0;
let editorCloseCounter1 = 0;
let editorCloseCounter = 0;
let editorPinCounter = 0;
let editorStickyCounter = 0;
const editorGroupChangeListener = group.onDidGroupChange(e => {
if (e.kind === GroupChangeKind.EDITOR_OPEN) {
assert.ok(e.editor);
@@ -380,16 +382,19 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
activeEditorChangeCounter++;
} else if (e.kind === GroupChangeKind.EDITOR_CLOSE) {
assert.ok(e.editor);
editorCloseCounter1++;
editorCloseCounter++;
} else if (e.kind === GroupChangeKind.EDITOR_PIN) {
assert.ok(e.editor);
editorPinCounter++;
} else if (e.kind === GroupChangeKind.EDITOR_STICKY) {
assert.ok(e.editor);
editorStickyCounter++;
}
});
let editorCloseCounter2 = 0;
let editorCloseCounter1 = 0;
const editorCloseListener = group.onDidCloseEditor(() => {
editorCloseCounter2++;
editorCloseCounter1++;
});
let editorWillCloseCounter = 0;
@@ -440,12 +445,18 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.closeEditor(inputInactive);
assert.equal(activeEditorChangeCounter, 3);
assert.equal(editorCloseCounter, 1);
assert.equal(editorCloseCounter1, 1);
assert.equal(editorCloseCounter2, 1);
assert.equal(editorWillCloseCounter, 1);
assert.equal(group.activeEditor, input);
assert.equal(editorStickyCounter, 0);
group.stickEditor(input);
assert.equal(editorStickyCounter, 1);
group.unstickEditor(input);
assert.equal(editorStickyCounter, 2);
editorCloseListener.dispose();
editorWillCloseListener.dispose();
editorWillOpenListener.dispose();
@@ -1021,4 +1032,49 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
editorGroupChangeListener.dispose();
part.dispose();
});
test('moveEditor with context (across groups)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const rightGroup = part.addGroup(group, GroupDirection.RIGHT);
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
let firstOpenEditorContext: OpenEditorContext | undefined;
Event.once(group.onWillOpenEditor)(e => {
firstOpenEditorContext = e.context;
});
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(firstOpenEditorContext, undefined);
const waitForEditorWillOpen = new Promise<OpenEditorContext | undefined>(c => {
Event.once(rightGroup.onWillOpenEditor)(e => c(e.context));
});
group.moveEditor(inputInactive, rightGroup, { index: 0 });
const context = await waitForEditorWillOpen;
assert.equal(context, OpenEditorContext.MOVE_EDITOR);
part.dispose();
});
test('copyEditor with context (across groups)', async () => {
const [part] = createPart();
const group = part.activeGroup;
assert.equal(group.isEmpty, true);
const rightGroup = part.addGroup(group, GroupDirection.RIGHT);
const input = new TestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
const inputInactive = new TestFileEditorInput(URI.file('foo/bar/inactive'), TEST_EDITOR_INPUT_ID);
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
const waitForEditorWillOpen = new Promise<OpenEditorContext | undefined>(c => {
Event.once(rightGroup.onWillOpenEditor)(e => c(e.context));
});
group.copyEditor(inputInactive, rightGroup, { index: 0 });
const context = await waitForEditorWillOpen;
assert.equal(context, OpenEditorContext.COPY_EDITOR);
part.dispose();
});
});

View File

@@ -28,6 +28,7 @@ import { UntitledTextEditorModel } from 'vs/workbench/services/untitled/common/u
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
import { isLinux } from 'vs/base/common/platform';
const TEST_EDITOR_ID = 'MyTestEditorForEditorService';
const TEST_EDITOR_INPUT_ID = 'testEditorInputForEditorService';
@@ -111,7 +112,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
assert.equal(visibleEditorChangeEventCounter, 1);
// Close input
await editor!.group!.closeEditor(input);
await editor?.group?.closeEditor(input);
assert.equal(0, service.count);
assert.equal(0, service.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE).length);
@@ -255,13 +256,13 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
const fileEditorInput1Again = service.createEditorInput({ resource: fileResource1 });
assert.equal(fileEditorInput1Again, fileEditorInput1);
fileEditorInput1Again!.dispose();
fileEditorInput1Again.dispose();
assert.ok(fileEditorInput1!.isDisposed());
assert.ok(fileEditorInput1.isDisposed());
const fileEditorInput1AgainAndAgain = service.createEditorInput({ resource: fileResource1 });
assert.notEqual(fileEditorInput1AgainAndAgain, fileEditorInput1);
assert.ok(!fileEditorInput1AgainAndAgain!.isDisposed());
assert.ok(!fileEditorInput1AgainAndAgain.isDisposed());
// Cached Input (Resource)
const resource1 = URI.from({ scheme: 'custom', path: '/foo/bar/cache1.js' });
@@ -277,13 +278,13 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
const input1Again = service.createEditorInput({ resource: resource1 });
assert.equal(input1Again, input1);
input1Again!.dispose();
input1Again.dispose();
assert.ok(input1!.isDisposed());
assert.ok(input1.isDisposed());
const input1AgainAndAgain = service.createEditorInput({ resource: resource1 });
assert.notEqual(input1AgainAndAgain, input1);
assert.ok(!input1AgainAndAgain!.isDisposed());
assert.ok(!input1AgainAndAgain.isDisposed());
});
test('createEditorInput', async function () {
@@ -301,6 +302,18 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
let contentInput = <FileEditorInput>input;
assert.strictEqual(contentInput.resource.fsPath, toResource.call(this, '/index.html').fsPath);
// Untyped Input (file casing)
input = service.createEditorInput({ resource: toResource.call(this, '/index.html') });
let inputDifferentCase = service.createEditorInput({ resource: toResource.call(this, '/INDEX.html') });
if (!isLinux) {
assert.equal(input, inputDifferentCase);
assert.equal(input.resource?.toString(), inputDifferentCase.resource?.toString());
} else {
assert.notEqual(input, inputDifferentCase);
assert.notEqual(input.resource?.toString(), inputDifferentCase.resource?.toString());
}
// Typed Input
assert.equal(service.createEditorInput(input), input);
assert.equal(service.createEditorInput({ editor: input }), input);
@@ -331,7 +344,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
input = service.createEditorInput({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledTextEditorInput);
let model = await input.resolve() as UntitledTextEditorModel;
assert.equal(model.textEditorModel!.getValue(), 'Hello Untitled');
assert.equal(model.textEditorModel?.getValue(), 'Hello Untitled');
// Untyped Input (untitled with mode)
input = service.createEditorInput({ mode, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
@@ -446,13 +459,13 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
assert.equal(part.activeGroup, rootGroup);
assert.equal(part.count, 2);
assert.equal(editor!.group, part.groups[1]);
assert.equal(editor?.group, part.groups[1]);
// Open to the side uses existing neighbour group if any
editor = await service.openEditor(input2, { pinned: true, preserveFocus: true }, SIDE_GROUP);
assert.equal(part.activeGroup, rootGroup);
assert.equal(part.count, 2);
assert.equal(editor!.group, part.groups[1]);
assert.equal(editor?.group, part.groups[1]);
part.dispose();
});
@@ -469,7 +482,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
await service.openEditor(input1, { pinned: true }, rootGroup);
let editor = await service.openEditor(input2, { pinned: true, preserveFocus: true, activation: EditorActivation.ACTIVATE }, SIDE_GROUP);
const sideGroup = editor!.group;
const sideGroup = editor?.group;
assert.equal(part.activeGroup, sideGroup);
@@ -527,7 +540,7 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
// 1.) open, open same, open other, close
let editor = await service.openEditor(input, { pinned: true });
const group = editor!.group!;
const group = editor?.group!;
assertActiveEditorChangedEvent(true);
assertVisibleEditorsChangedEvent(true);
@@ -1059,4 +1072,24 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
handler.dispose();
part.dispose();
});
test('whenClosed', async function () {
const [part, service] = createEditorService();
const input1 = new TestFileEditorInput(URI.parse('file://resource1'), TEST_EDITOR_INPUT_ID);
const input2 = new TestFileEditorInput(URI.parse('file://resource2'), TEST_EDITOR_INPUT_ID);
await part.whenRestored;
const editor = await service.openEditor(input1, { pinned: true });
await service.openEditor(input2, { pinned: true });
const whenClosed = service.whenClosed([input1.resource, input2.resource]);
editor?.group?.closeAllEditors();
await whenClosed;
part.dispose();
});
});

View File

@@ -1,25 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
export class ElectronService {
_serviceBrand: undefined;
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService
) {
return createChannelSender<IElectronService>(mainProcessService.getChannel('electron'), { context: environmentService.configuration.windowId });
}
}
registerSingleton(IElectronService, ElectronService, true);

View File

@@ -14,6 +14,8 @@ import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
import product from 'vs/platform/product/common/product';
import { memoize } from 'vs/base/common/decorators';
import { onUnexpectedError } from 'vs/base/common/errors';
import { LIGHT } from 'vs/platform/theme/common/themeService';
import { parseLineAndColumnAware } from 'vs/base/common/extpath';
export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguration {
@@ -37,7 +39,20 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio
if (this.payload) {
const fileToOpen = this.payload.get('openFile');
if (fileToOpen) {
return [{ fileUri: URI.parse(fileToOpen) }];
const fileUri = URI.parse(fileToOpen);
// Support: --goto parameter to open on line/col
if (this.payload.has('gotoLineMode')) {
const pathColumnAware = parseLineAndColumnAware(fileUri.path);
return [{
fileUri: fileUri.with({ path: pathColumnAware.path }),
lineNumber: pathColumnAware.line,
columnNumber: pathColumnAware.column
}];
}
return [{ fileUri }];
}
}
@@ -63,6 +78,10 @@ export class BrowserEnvironmentConfiguration implements IEnvironmentConfiguratio
get highContrast() {
return false; // could investigate to detect high contrast theme automatically
}
get defaultThemeType() {
return LIGHT;
}
}
interface IBrowserWorkbenchEnvironmentConstructionOptions extends IWorkbenchConstructionOptions {

View File

@@ -42,10 +42,10 @@ export class NativeWorkbenchEnvironmentService extends EnvironmentService implem
}
@memoize
get webviewResourceRoot(): string { return 'vscode-resource://{{resource}}'; }
get webviewResourceRoot(): string { return `${Schemas.vscodeWebviewResource}://{{uuid}}/{{resource}}`; }
@memoize
get webviewCspSource(): string { return 'vscode-resource:'; }
get webviewCspSource(): string { return `${Schemas.vscodeWebviewResource}:`; }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }

View File

@@ -140,8 +140,8 @@ export class ExtensionManagementService extends Disposable implements IExtension
return Promise.reject(`Invalid location ${extension.location.toString()}`);
}
unzip(zipLocation: URI, type: ExtensionType): Promise<IExtensionIdentifier> {
return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation, type))).then(([extensionIdentifier]) => extensionIdentifier);
unzip(zipLocation: URI): Promise<IExtensionIdentifier> {
return Promise.all(this.servers.map(({ extensionManagementService }) => extensionManagementService.unzip(zipLocation))).then(([extensionIdentifier]) => extensionIdentifier);
}
async install(vsix: URI): Promise<ILocalExtension> {

View File

@@ -6,7 +6,7 @@
import * as nls from 'vs/nls';
import { ChildProcess, fork } from 'child_process';
import { Server, Socket, createServer } from 'net';
import { CrashReporterStartOptions } from 'electron';
import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { timeout } from 'vs/base/common/async';
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -28,7 +28,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import product from 'vs/platform/product/common/product';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IInitData, UIKind } from 'vs/workbench/api/common/extHost.protocol';
import { MessageType, createMessageOfType, isMessageOfType } from 'vs/workbench/services/extensions/common/extensionHostProtocol';

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcRenderer as ipc } from 'electron';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { ExtensionHostProcessWorker } from 'vs/workbench/services/extensions/electron-browser/extensionHost';
import { CachedExtensionScanner } from 'vs/workbench/services/extensions/electron-browser/cachedExtensionScanner';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
@@ -34,7 +34,7 @@ import { IProductService } from 'vs/platform/product/common/productService';
import { Logger } from 'vs/workbench/services/extensions/common/extensionPoints';
import { flatten } from 'vs/base/common/arrays';
import { IStaticExtensionsService } from 'vs/workbench/services/extensions/common/staticExtensions';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IRemoteExplorerService } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { Action } from 'vs/base/common/actions';
@@ -578,7 +578,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
public _onExtensionHostExit(code: number): void {
if (this._isExtensionDevTestFromCli) {
// When CLI testing make sure to exit with proper exit code
ipc.send('vscode:exit', code);
ipcRenderer.send('vscode:exit', code);
} else {
// Expected development extension termination: When the extension host goes down we also shutdown the window
this._electronService.closeWindow();
@@ -619,7 +619,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
}
} else {
// Install the Extension and reload the window to handle.
const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nOK to install?", recommendation.friendlyName);
const message = nls.localize('installResolver', "Extension '{0}' is required to open the remote window.\nDo you want to install the extension?", recommendation.friendlyName);
this._notificationService.prompt(Severity.Info, message,
[{
label: nls.localize('install', 'Install and Reload'),

View File

@@ -383,7 +383,7 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
notices.push(nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents'));
return false;
}
if (typeof extensionDescription.main === 'undefined') {
if (typeof extensionDescription.main === 'undefined' && typeof extensionDescription.browser === 'undefined') {
notices.push(nls.localize('extensionDescription.activationEvents2', "properties `{0}` and `{1}` must both be specified or must both be omitted", 'activationEvents', 'main'));
return false;
}

View File

@@ -5,7 +5,7 @@
import { URI, UriComponents } from 'vs/base/common/uri';
import { IEditor } from 'vs/editor/common/editorCommon';
import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
import { ITextEditorOptions, IResourceEditorInput, TextEditorSelectionRevealType, IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditorPane, Extensions as EditorExtensions, EditorInput, IEditorCloseEvent, IEditorInputFactoryRegistry, toResource, IEditorIdentifier, GroupIdentifier, EditorsOrder } from 'vs/workbench/common/editor';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
@@ -638,10 +638,22 @@ export class HistoryService extends Disposable implements IHistoryService {
if (lastClosedFile) {
(async () => {
const editor = await this.editorService.openEditor({
resource: lastClosedFile.resource,
options: { pinned: true, sticky: lastClosedFile.sticky, index: lastClosedFile.index }
});
let options: IEditorOptions;
if (lastClosedFile.sticky) {
// Sticky: in case the target index is outside of the range of
// sticky editors, we make sure to not provide the index as
// option. Otherwise the index will cause the sticky flag to
// be ignored.
if (!this.editorGroupService.activeGroup.isSticky(lastClosedFile.index)) {
options = { pinned: true, sticky: true };
} else {
options = { pinned: true, sticky: true, index: lastClosedFile.index };
}
} else {
options = { pinned: true, index: lastClosedFile.index };
}
const editor = await this.editorService.openEditor({ resource: lastClosedFile.resource, options });
// Fix for https://github.com/Microsoft/vscode/issues/67882
// If opening of the editor fails, make sure to try the next one

View File

@@ -7,9 +7,9 @@ import { Event } from 'vs/base/common/event';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
import { IResourceEditorInputType, IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { IWindowSettings, IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, isFileToOpen, IOpenEmptyWindowOptions, IPathData, IFileToOpen } from 'vs/platform/windows/common/windows';
import { pathsToEditors } from 'vs/workbench/common/editor';
import { IFileService } from 'vs/platform/files/common/files';
import { ILabelService } from 'vs/platform/label/common/label';
@@ -19,6 +19,10 @@ import { URI } from 'vs/base/common/uri';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { domEvent } from 'vs/base/browser/event';
import { memoize } from 'vs/base/common/decorators';
import { parseLineAndColumnAware } from 'vs/base/common/extpath';
import { IWorkspaceFolderCreationData } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
/**
* A workspace to open in the workbench can either be:
@@ -65,7 +69,8 @@ export class BrowserHostService extends Disposable implements IHostService {
@IConfigurationService private readonly configurationService: IConfigurationService,
@IFileService private readonly fileService: IFileService,
@ILabelService private readonly labelService: ILabelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IInstantiationService private readonly instantiationService: IInstantiationService
) {
super();
@@ -113,60 +118,142 @@ export class BrowserHostService extends Disposable implements IHostService {
}
private async doOpenWindow(toOpen: IWindowOpenable[], options?: IOpenWindowOptions): Promise<void> {
for (let i = 0; i < toOpen.length; i++) {
const openable = toOpen[i];
openable.label = openable.label || this.getRecentLabel(openable);
const payload = this.preservePayload();
const fileOpenables: IFileToOpen[] = [];
const foldersToAdd: IWorkspaceFolderCreationData[] = [];
// selectively copy payload: for now only extension debugging properties are considered
const originalPayload = this.workspaceProvider.payload;
let newPayload: Array<unknown> | undefined = undefined;
if (originalPayload && Array.isArray(originalPayload)) {
for (let pair of originalPayload) {
if (Array.isArray(pair) && pair.length === 2) {
switch (pair[0]) {
case 'extensionDevelopmentPath':
case 'debugId':
case 'inspect-brk-extensions':
if (!newPayload) {
newPayload = new Array();
}
newPayload.push(pair);
break;
}
}
}
}
for (const openable of toOpen) {
openable.label = openable.label || this.getRecentLabel(openable);
// Folder
if (isFolderToOpen(openable)) {
this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload: newPayload });
if (options?.addMode) {
foldersToAdd.push(({ uri: openable.folderUri }));
} else {
this.workspaceProvider.open({ folderUri: openable.folderUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });
}
}
// Workspace
else if (isWorkspaceToOpen(openable)) {
this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload: newPayload });
this.workspaceProvider.open({ workspaceUri: openable.workspaceUri }, { reuse: this.shouldReuse(options, false /* no file */), payload });
}
// File
// File (handled later in bulk)
else if (isFileToOpen(openable)) {
fileOpenables.push(openable);
}
}
// Handle Folders to Add
if (foldersToAdd.length > 0) {
this.instantiationService.invokeFunction(accessor => {
const workspaceEditingService: IWorkspaceEditingService = accessor.get(IWorkspaceEditingService);
workspaceEditingService.addFolders(foldersToAdd);
});
}
// Handle Files
if (fileOpenables.length > 0) {
// Support diffMode
if (options?.diffMode && fileOpenables.length === 2) {
const editors = await pathsToEditors(fileOpenables, this.fileService);
if (editors.length !== 2 || !editors[0].resource || !editors[1].resource) {
return; // invalid resources
}
// Same Window: open via editor service in current window
if (this.shouldReuse(options, true /* file */)) {
const inputs: IResourceEditorInputType[] = await pathsToEditors([openable], this.fileService);
this.editorService.openEditors(inputs);
this.editorService.openEditor({
leftResource: editors[0].resource,
rightResource: editors[1].resource
});
}
// New Window: open into empty window
else {
const environment = new Map<string, string>();
environment.set('openFile', openable.fileUri.toString());
environment.set('diffFileDetail', editors[0].resource.toString());
environment.set('diffFileMaster', editors[1].resource.toString());
this.workspaceProvider.open(undefined, { payload: Array.from(environment.entries()) });
}
}
// Just open normally
else {
for (const openable of fileOpenables) {
// Same Window: open via editor service in current window
if (this.shouldReuse(options, true /* file */)) {
let openables: IPathData[] = [];
// Support: --goto parameter to open on line/col
if (options?.gotoLineMode) {
const pathColumnAware = parseLineAndColumnAware(openable.fileUri.path);
openables = [{
fileUri: openable.fileUri.with({ path: pathColumnAware.path }),
lineNumber: pathColumnAware.line,
columnNumber: pathColumnAware.column
}];
} else {
openables = [openable];
}
this.editorService.openEditors(await pathsToEditors(openables, this.fileService));
}
// New Window: open into empty window
else {
const environment = new Map<string, string>();
environment.set('openFile', openable.fileUri.toString());
if (options?.gotoLineMode) {
environment.set('gotoLineMode', 'true');
}
this.workspaceProvider.open(undefined, { payload: Array.from(environment.entries()) });
}
}
}
// Support wait mode
const waitMarkerFileURI = options?.waitMarkerFileURI;
if (waitMarkerFileURI) {
(async () => {
// Wait for the resources to be closed in the editor...
await this.editorService.whenClosed(fileOpenables.map(openable => openable.fileUri), { waitForSaved: true });
// ...before deleting the wait marker file
await this.fileService.del(waitMarkerFileURI);
})();
}
}
}
private preservePayload(): Array<unknown> | undefined {
// Selectively copy payload: for now only extension debugging properties are considered
let newPayload: Array<unknown> | undefined = undefined;
if (this.environmentService.extensionDevelopmentLocationURI) {
newPayload = new Array();
newPayload.push(['extensionDevelopmentPath', this.environmentService.extensionDevelopmentLocationURI.toString()]);
if (this.environmentService.debugExtensionHost.debugId) {
newPayload.push(['debugId', this.environmentService.debugExtensionHost.debugId]);
}
if (this.environmentService.debugExtensionHost.port) {
newPayload.push(['inspect-brk-extensions', String(this.environmentService.debugExtensionHost.port)]);
}
}
return newPayload;
}
private getRecentLabel(openable: IWindowOpenable): string {
if (isFolderToOpen(openable)) {
return this.labelService.getWorkspaceLabel(openable.folderUri, { verbose: true });
@@ -179,7 +266,11 @@ export class BrowserHostService extends Disposable implements IHostService {
return this.labelService.getUriLabel(openable.fileUri);
}
private shouldReuse(options: IOpenWindowOptions = {}, isFile: boolean): boolean {
private shouldReuse(options: IOpenWindowOptions = Object.create(null), isFile: boolean): boolean {
if (options.waitMarkerFileURI) {
return true; // always handle --wait in same window
}
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const openInNewWindowConfig = isFile ? (windowConfig?.openFilesInNewWindow || 'off' /* default */) : (windowConfig?.openFoldersInNewWindow || 'default' /* default */);

View File

@@ -5,13 +5,12 @@
import { Event } from 'vs/base/common/event';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILabelService } from 'vs/platform/label/common/label';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWindowOpenable, IOpenWindowOptions, isFolderToOpen, isWorkspaceToOpen, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
import { Disposable } from 'vs/base/common/lifecycle';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
export class DesktopHostService extends Disposable implements IHostService {
@@ -20,15 +19,15 @@ export class DesktopHostService extends Disposable implements IHostService {
constructor(
@IElectronService private readonly electronService: IElectronService,
@ILabelService private readonly labelService: ILabelService,
@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
) {
super();
}
get onDidChangeFocus(): Event<boolean> { return this._onDidChangeFocus; }
private _onDidChangeFocus: Event<boolean> = Event.latch(Event.any(
Event.map(Event.filter(this.electronService.onWindowFocus, id => id === this.environmentService.configuration.windowId), () => this.hasFocus),
Event.map(Event.filter(this.electronService.onWindowBlur, id => id === this.environmentService.configuration.windowId), () => this.hasFocus)
Event.map(Event.filter(this.electronService.onWindowFocus, id => id === this.electronService.windowId), () => this.hasFocus),
Event.map(Event.filter(this.electronService.onWindowBlur, id => id === this.electronService.windowId), () => this.hasFocus)
));
get hasFocus(): boolean {
@@ -42,7 +41,7 @@ export class DesktopHostService extends Disposable implements IHostService {
return false;
}
return activeWindowId === this.environmentService.configuration.windowId;
return activeWindowId === this.electronService.windowId;
}
openWindow(options?: IOpenEmptyWindowOptions): Promise<void>;

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IIssueService } from 'vs/platform/issue/node/issue';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IIssueService } from 'vs/platform/issue/electron-sandbox/issue';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class IssueService {

View File

@@ -79,7 +79,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
if (keybindingItem.isDefault && keybindingItem.resolvedKeybinding) {
this.removeDefaultKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
return this.save().finally(() => reference.dispose());
});
}
@@ -92,7 +92,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
} else {
this.removeUserKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
return this.save().finally(() => reference.dispose());
});
}
@@ -104,7 +104,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
this.removeUserKeybinding(keybindingItem, model);
this.removeUnassignedDefaultKeybinding(keybindingItem, model);
}
return this.save().then(() => reference.dispose());
return this.save().finally(() => reference.dispose());
});
}
@@ -230,10 +230,12 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
if (model.getValue()) {
const parsed = this.parse(model);
if (parsed.parseErrors.length) {
reference.dispose();
return Promise.reject<any>(new Error(localize('parseErrors', "Unable to write to the keybindings configuration file. Please open it to correct errors/warnings in the file and try again.")));
}
if (parsed.result) {
if (!isArray(parsed.result)) {
reference.dispose();
return Promise.reject<any>(new Error(localize('errorInvalidConfiguration', "Unable to write to the keybindings configuration file. It has an object which is not of type Array. Please open the file to clean up and try again.")));
}
} else {

View File

@@ -22,12 +22,10 @@ export class BrowserLifecycleService extends AbstractLifecycleService {
}
private registerListeners(): void {
// Note: we cannot change this to window.addEventListener('beforeUnload')
// because it seems that mechanism does not allow for preventing the unload
window.onbeforeunload = () => this.onBeforeUnload();
window.addEventListener('beforeunload', e => this.onBeforeUnload(e));
}
private onBeforeUnload(): string | null {
private onBeforeUnload(event: BeforeUnloadEvent): void {
const logService = this.logService;
logService.info('[lifecycle] onBeforeUnload triggered');
@@ -48,7 +46,10 @@ export class BrowserLifecycleService extends AbstractLifecycleService {
// Veto: signal back to browser by returning a non-falsify return value
if (veto) {
return localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again.");
event.preventDefault();
event.returnValue = localize('lifecycleVeto', "Changes that you made may not be saved. Please check press 'Cancel' and try again.");
return;
}
// No Veto: continue with Will Shutdown
@@ -61,8 +62,6 @@ export class BrowserLifecycleService extends AbstractLifecycleService {
// Finally end with Shutdown event
this._onShutdown.fire();
return null;
}
}

View File

@@ -3,19 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { ShutdownReason, StartupKind, handleVetos, ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
import { ipcRenderer as ipc } from 'electron';
import { ipcRenderer } from 'vs/base/parts/sandbox/electron-sandbox/globals';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { onUnexpectedError } from 'vs/base/common/errors';
import { AbstractLifecycleService } from 'vs/platform/lifecycle/common/lifecycleService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import Severity from 'vs/base/common/severity';
import { localize } from 'vs/nls';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class NativeLifecycleService extends AbstractLifecycleService {
@@ -27,7 +26,7 @@ export class NativeLifecycleService extends AbstractLifecycleService {
constructor(
@INotificationService private readonly notificationService: INotificationService,
@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
@IElectronService private readonly electronService: IElectronService,
@IStorageService readonly storageService: IStorageService,
@ILogService readonly logService: ILogService
) {
@@ -57,10 +56,10 @@ export class NativeLifecycleService extends AbstractLifecycleService {
}
private registerListeners(): void {
const windowId = this.environmentService.configuration.windowId;
const windowId = this.electronService.windowId;
// Main side indicates that window is about to unload, check for vetos
ipc.on('vscode:onBeforeUnload', (_event: unknown, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => {
ipcRenderer.on('vscode:onBeforeUnload', (event: unknown, reply: { okChannel: string, cancelChannel: string, reason: ShutdownReason }) => {
this.logService.trace(`lifecycle: onBeforeUnload (reason: ${reply.reason})`);
// trigger onBeforeShutdown events and veto collecting
@@ -68,18 +67,18 @@ export class NativeLifecycleService extends AbstractLifecycleService {
if (veto) {
this.logService.trace('lifecycle: onBeforeUnload prevented via veto');
ipc.send(reply.cancelChannel, windowId);
ipcRenderer.send(reply.cancelChannel, windowId);
} else {
this.logService.trace('lifecycle: onBeforeUnload continues without veto');
this.shutdownReason = reply.reason;
ipc.send(reply.okChannel, windowId);
ipcRenderer.send(reply.okChannel, windowId);
}
});
});
// Main side indicates that we will indeed shutdown
ipc.on('vscode:onWillUnload', async (_event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => {
ipcRenderer.on('vscode:onWillUnload', async (event: unknown, reply: { replyChannel: string, reason: ShutdownReason }) => {
this.logService.trace(`lifecycle: onWillUnload (reason: ${reply.reason})`);
// trigger onWillShutdown events and joining
@@ -89,7 +88,7 @@ export class NativeLifecycleService extends AbstractLifecycleService {
this._onShutdown.fire();
// acknowledge to main side
ipc.send(reply.replyChannel, windowId);
ipcRenderer.send(reply.replyChannel, windowId);
});
// Save shutdown reason to retrieve on next startup

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';

View File

@@ -8,7 +8,7 @@ import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapab
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter } from 'vs/base/common/event';
import { VSBuffer } from 'vs/base/common/buffer';
import { isEqualOrParent, joinPath, relativePath } from 'vs/base/common/resources';
import { joinPath, extUri } from 'vs/base/common/resources';
import { values } from 'vs/base/common/map';
import { localize } from 'vs/nls';
@@ -65,8 +65,8 @@ export abstract class KeyValueLogProvider extends Disposable implements IFileSys
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
for (const key of keys) {
const keyResource = this.toResource(key);
if (isEqualOrParent(keyResource, resource, false)) {
const path = relativePath(resource, keyResource, false);
if (extUri.isEqualOrParent(keyResource, resource)) {
const path = extUri.relativePath(resource, keyResource);
if (path) {
const keySegments = path.split('/');
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);

View File

@@ -6,7 +6,7 @@
import { DelegatedLogService, ILogService, ConsoleLogInMainService, ConsoleLogService, MultiplexLogService } from 'vs/platform/log/common/log';
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { LoggerChannelClient, FollowerLogService } from 'vs/platform/log/common/logIpc';
import { SpdLogService } from 'vs/platform/log/node/spdlogService';
import { DisposableStore } from 'vs/base/common/lifecycle';

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMenubarService } from 'vs/platform/menubar/node/menubar';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IMenubarService } from 'vs/platform/menubar/electron-sandbox/menubar';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class MenubarService {

View File

@@ -22,6 +22,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
class OutputChannelBackedByFile extends AbstractFileOutputChannelModel implements IOutputChannelModel {
@@ -204,7 +205,8 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService
constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
@IFileService private readonly fileService: IFileService
@IFileService private readonly fileService: IFileService,
@IElectronService private readonly electronService: IElectronService
) {
super(instantiationService);
}
@@ -217,7 +219,7 @@ export class OutputChannelModelService extends AsbtractOutputChannelModelService
private _outputDir: Promise<URI> | null = null;
private get outputDir(): Promise<URI> {
if (!this._outputDir) {
const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.environmentService.configuration.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`));
const outputDir = URI.file(join(this.environmentService.logsPath, `output_${this.electronService.windowId}_${toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '')}`));
this._outputDir = this.fileService.createFolder(outputDir).then(() => outputDir);
}
return this._outputDir;

View File

@@ -518,7 +518,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return this._defaultUserSettingsContentModel;
}
private async getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null> {
public async getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null> {
switch (configurationTarget) {
case ConfigurationTarget.USER:
case ConfigurationTarget.USER_LOCAL:

View File

@@ -421,22 +421,6 @@ class KeybindingItemMatches {
return this.wordMatchesMetaModifier(word);
}
private wordMatchesMetaModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.metaKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.metaKey, word)) {
return true;
}
if (matchesPrefix(localize('meta', "meta"), word)) {
return true;
}
return false;
}
private matchesCtrlModifier(keybinding: ResolvedKeybindingPart | null, word: string): boolean {
if (!keybinding) {
return false;
@@ -447,19 +431,6 @@ class KeybindingItemMatches {
return this.wordMatchesCtrlModifier(word);
}
private wordMatchesCtrlModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.ctrlKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.ctrlKey, word)) {
return true;
}
return false;
}
private matchesShiftModifier(keybinding: ResolvedKeybindingPart | null, word: string): boolean {
if (!keybinding) {
return false;
@@ -470,19 +441,6 @@ class KeybindingItemMatches {
return this.wordMatchesShiftModifier(word);
}
private wordMatchesShiftModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.shiftKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.shiftKey, word)) {
return true;
}
return false;
}
private matchesAltModifier(keybinding: ResolvedKeybindingPart | null, word: string): boolean {
if (!keybinding) {
return false;
@@ -493,22 +451,6 @@ class KeybindingItemMatches {
return this.wordMatchesAltModifier(word);
}
private wordMatchesAltModifier(word: string): boolean {
if (matchesPrefix(this.modifierLabels.ui.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.aria.altKey, word)) {
return true;
}
if (matchesPrefix(this.modifierLabels.user.altKey, word)) {
return true;
}
if (matchesPrefix(localize('option', "option"), word)) {
return true;
}
return false;
}
private hasAnyMatch(keybindingMatch: KeybindingMatch): boolean {
return !!keybindingMatch.altKey ||
!!keybindingMatch.ctrlKey ||
@@ -574,4 +516,62 @@ class KeybindingItemMatches {
}
return false;
}
private wordMatchesAltModifier(word: string): boolean {
if (strings.equalsIgnoreCase(this.modifierLabels.ui.altKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.aria.altKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.user.altKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(localize('option', "option"), word)) {
return true;
}
return false;
}
private wordMatchesCtrlModifier(word: string): boolean {
if (strings.equalsIgnoreCase(this.modifierLabels.ui.ctrlKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.aria.ctrlKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.user.ctrlKey, word)) {
return true;
}
return false;
}
private wordMatchesMetaModifier(word: string): boolean {
if (strings.equalsIgnoreCase(this.modifierLabels.ui.metaKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.aria.metaKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.user.metaKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(localize('meta', "meta"), word)) {
return true;
}
return false;
}
private wordMatchesShiftModifier(word: string): boolean {
if (strings.equalsIgnoreCase(this.modifierLabels.ui.shiftKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.aria.shiftKey, word)) {
return true;
}
if (strings.equalsIgnoreCase(this.modifierLabels.user.shiftKey, word)) {
return true;
}
return false;
}
}

View File

@@ -207,6 +207,7 @@ export interface IPreferencesService {
switchSettings(target: ConfigurationTarget, resource: URI, jsonEditor?: boolean): Promise<void>;
openGlobalKeybindingSettings(textual: boolean): Promise<void>;
openDefaultKeybindingsFile(): Promise<IEditorPane | undefined>;
getEditableSettingsURI(configurationTarget: ConfigurationTarget, resource?: URI): Promise<URI | null>;
}
export function getSettingsTargetName(target: ConfigurationTarget, resource: URI, workspaceContextService: IWorkspaceContextService): string {

View File

@@ -55,31 +55,41 @@ export function createValidator(prop: IConfigurationPropertySchema): (value: any
};
}
/**
* Returns an error string if the value is invalid and can't be displayed in the settings UI for the given type.
*/
export function getInvalidTypeError(value: any, type: undefined | string | string[]): string | undefined {
let typeArr = Array.isArray(type) ? type : [type];
const isNullable = canBeType(typeArr, 'null');
if (canBeType(typeArr, 'number', 'integer') && (typeArr.length === 1 || typeArr.length === 2 && isNullable)) {
if (value === '' || isNaN(+value)) {
return nls.localize('validations.expectedNumeric', "Value must be a number.");
}
if (typeof type === 'undefined') {
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
}
const valueType = typeof value;
if (
(valueType === 'boolean' && !canBeType(typeArr, 'boolean')) ||
(valueType === 'object' && !canBeType(typeArr, 'object', 'null', 'array')) ||
(valueType === 'string' && !canBeType(typeArr, 'string', 'number', 'integer')) ||
(typeof parseFloat(value) === 'number' && !isNaN(parseFloat(value)) && !canBeType(typeArr, 'number', 'integer')) ||
(Array.isArray(value) && !canBeType(typeArr, 'array'))
) {
if (typeof type !== 'undefined') {
return nls.localize('invalidTypeError', "Setting has an invalid type, expected {0}. Fix in JSON.", JSON.stringify(type));
}
const typeArr = Array.isArray(type) ? type : [type];
if (!typeArr.some(_type => valueValidatesAsType(value, _type))) {
return nls.localize('invalidTypeError', "Setting has an invalid type, expected {0}. Fix in JSON.", JSON.stringify(type));
}
return undefined; // {{SQL CARBON EDIT}} strict-null-checks
}
function valueValidatesAsType(value: any, type: string): boolean {
const valueType = typeof value;
if (type === 'boolean') {
return valueType === 'boolean';
} else if (type === 'object') {
return value && !Array.isArray(value) && valueType === 'object';
} else if (type === 'null') {
return value === null;
} else if (type === 'array') {
return Array.isArray(value);
} else if (type === 'string') {
return valueType === 'string';
} else if (type === 'number' || type === 'integer') {
return valueType === 'number';
}
return true;
}
function getStringValidators(prop: IConfigurationPropertySchema) {
let patternRegex: RegExp | undefined;
if (typeof prop.pattern === 'string') {

View File

@@ -34,7 +34,7 @@ class AnAction extends Action {
}
}
suite('KeybindingsEditorModel test', () => {
suite('KeybindingsEditorModel', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditorModel;
@@ -568,6 +568,46 @@ suite('KeybindingsEditorModel test', () => {
assert.deepEqual(actual[0].keybindingMatches!.firstPart, { keyCode: true });
});
test('filter modifiers are not matched when not completely matched (prefix)', async () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const term = `alt.${uuid.generateUuid()}`;
const command = `command.${term}`;
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command: 'some_command', firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: false }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch(term);
assert.equal(1, actual.length);
assert.equal(command, actual[0].keybindingItem.command);
assert.equal(1, actual[0].commandIdMatches?.length);
});
test('filter modifiers are not matched when not completely matched (includes)', async () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const term = `abcaltdef.${uuid.generateUuid()}`;
const command = `command.${term}`;
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape }, isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command: 'some_command', firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: false }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch(term);
assert.equal(1, actual.length);
assert.equal(command, actual[0].keybindingItem.command);
assert.equal(1, actual[0].commandIdMatches?.length);
});
test('filter modifiers are matched with complete term', async () => {
testObject = instantiationService.createInstance(KeybindingsEditorModel, OperatingSystem.Macintosh);
const command = `command.${uuid.generateUuid()}`;
const expected = aResolvedKeybindingItem({ command, firstPart: { keyCode: KeyCode.Escape, modifiers: { altKey: true } }, isDefault: false });
prepareKeybindingService(expected, aResolvedKeybindingItem({ command: 'some_command', firstPart: { keyCode: KeyCode.Escape }, isDefault: false }));
await testObject.resolve(new Map<string, string>());
const actual = testObject.fetch('alt').filter(element => element.keybindingItem.command === command);
assert.equal(1, actual.length);
assert.deepEqual(actual[0].keybindingMatches!.firstPart, { altKey: true });
});
function prepareKeybindingService(...keybindingItems: ResolvedKeybindingItem[]): ResolvedKeybindingItem[] {
instantiationService.stub(IKeybindingService, 'getKeybindings', () => keybindingItems);
instantiationService.stub(IKeybindingService, 'getDefaultKeybindings', () => keybindingItems);

View File

@@ -5,10 +5,10 @@
import * as assert from 'assert';
import { IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry';
import { createValidator } from 'vs/workbench/services/preferences/common/preferencesValidation';
import { createValidator, getInvalidTypeError } from 'vs/workbench/services/preferences/common/preferencesValidation';
suite('Preferences Model test', () => {
suite('Preferences Validation', () => {
class Tester {
private validator: (value: any) => string | null;
@@ -334,4 +334,43 @@ suite('Preferences Model test', () => {
arr.rejects(['a', 'a']).withMessage(`Array has duplicate items`);
});
test('getInvalidTypeError', () => {
function testInvalidTypeError(value: any, type: string | string[], shouldValidate: boolean) {
const message = `value: ${value}, type: ${JSON.stringify(type)}, expected: ${shouldValidate ? 'valid' : 'invalid'}`;
if (shouldValidate) {
assert.ok(!getInvalidTypeError(value, type), message);
} else {
assert.ok(getInvalidTypeError(value, type), message);
}
}
testInvalidTypeError(1, 'number', true);
testInvalidTypeError(1.5, 'number', true);
testInvalidTypeError([1], 'number', false);
testInvalidTypeError('1', 'number', false);
testInvalidTypeError({ a: 1 }, 'number', false);
testInvalidTypeError(null, 'number', false);
testInvalidTypeError('a', 'string', true);
testInvalidTypeError('1', 'string', true);
testInvalidTypeError([], 'string', false);
testInvalidTypeError({}, 'string', false);
testInvalidTypeError([1], 'array', true);
testInvalidTypeError([], 'array', true);
testInvalidTypeError([{}, [[]]], 'array', true);
testInvalidTypeError({ a: ['a'] }, 'array', false);
testInvalidTypeError('hello', 'array', false);
testInvalidTypeError(true, 'boolean', true);
testInvalidTypeError('hello', 'boolean', false);
testInvalidTypeError(null, 'boolean', false);
testInvalidTypeError([true], 'boolean', false);
testInvalidTypeError(null, 'null', true);
testInvalidTypeError(false, 'null', false);
testInvalidTypeError([null], 'null', false);
testInvalidTypeError('null', 'null', false);
});
});

View File

@@ -8,7 +8,7 @@ import { ILogService } from 'vs/platform/log/common/log';
import { RequestService } from 'vs/platform/request/browser/requestService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IRequestService } from 'vs/platform/request/common/request';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class NativeRequestService extends RequestService {

View File

@@ -183,7 +183,7 @@ export function resultIsMatch(result: ITextSearchResult): result is ITextSearchM
}
export interface IProgressMessage {
message?: string;
message: string;
}
export type ISearchProgressItem = IFileMatch | IProgressMessage;
@@ -192,8 +192,8 @@ export function isFileMatch(p: ISearchProgressItem): p is IFileMatch {
return !!(<IFileMatch>p).resource;
}
export function isProgressMessage(p: ISearchProgressItem): p is IProgressMessage {
return !isFileMatch(p);
export function isProgressMessage(p: ISearchProgressItem | ISerializedSearchProgressItem): p is IProgressMessage {
return !!(p as IProgressMessage).message;
}
export interface ISearchCompleteStats {
@@ -350,6 +350,7 @@ export interface ISearchConfigurationProperties {
searchEditor: {
doubleClickBehaviour: 'selectWord' | 'goToLocation' | 'openLocationToSide',
reusePriorSearchConfiguration: boolean,
defaultShowContextValue: number | null,
experimental: {}
};
sortOrder: SearchSortOrder;
@@ -468,7 +469,7 @@ export interface IRawFileMatch {
*
* If not given, the search algorithm should use `relativePath`.
*/
searchPath?: string;
searchPath: string | undefined;
}
export interface ISearchEngine<T> {

View File

@@ -116,41 +116,43 @@ export class SearchService extends Disposable implements ISearchService {
schemesInQuery.forEach(scheme => providerActivations.push(this.extensionService.activateByEvent(`onSearch:${scheme}`)));
providerActivations.push(this.extensionService.activateByEvent('onSearch:file'));
const providerPromise = Promise.all(providerActivations)
.then(() => this.extensionService.whenInstalledExtensionsRegistered())
.then(() => {
// Cancel faster if search was canceled while waiting for extensions
const providerPromise = (async () => {
await Promise.all(providerActivations);
this.extensionService.whenInstalledExtensionsRegistered();
// Cancel faster if search was canceled while waiting for extensions
if (token && token.isCancellationRequested) {
return Promise.reject(canceled());
}
const progressCallback = (item: ISearchProgressItem) => {
if (token && token.isCancellationRequested) {
return Promise.reject(canceled());
return;
}
const progressCallback = (item: ISearchProgressItem) => {
if (token && token.isCancellationRequested) {
return;
}
if (onProgress) {
onProgress(item);
}
};
return this.searchWithProviders(query, progressCallback, token);
})
.then(completes => {
completes = arrays.coalesce(completes);
if (!completes.length) {
return {
limitHit: false,
results: []
};
if (onProgress) {
onProgress(item);
}
};
return <ISearchComplete>{
limitHit: completes[0] && completes[0].limitHit,
stats: completes[0].stats,
results: arrays.flatten(completes.map((c: ISearchComplete) => c.results))
const exists = await Promise.all(query.folderQueries.map(query => this.fileService.exists(query.folder)));
query.folderQueries = query.folderQueries.filter((_, i) => exists[i]);
let completes = await this.searchWithProviders(query, progressCallback, token);
completes = arrays.coalesce(completes);
if (!completes.length) {
return {
limitHit: false,
results: []
};
});
}
return <ISearchComplete>{
limitHit: completes[0] && completes[0].limitHit,
stats: completes[0].stats,
results: arrays.flatten(completes.map((c: ISearchComplete) => c.results))
};
})();
return new Promise((resolve, reject) => {
if (token) {

View File

@@ -24,9 +24,8 @@ import { IFileQuery, IFolderQuery, IProgressMessage, ISearchEngineStats, IRawFil
import { spawnRipgrepCmd } from './ripgrepFileSearch';
import { prepareQuery } from 'vs/base/common/fuzzyScorer';
interface IDirectoryEntry {
interface IDirectoryEntry extends IRawFileMatch {
base: string;
relativePath: string;
basename: string;
}
@@ -122,7 +121,7 @@ export class FileWalker {
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */ });
this.matchFile(onResult, { relativePath: extraFilePath.fsPath /* no workspace relative path */, searchPath: undefined });
});
this.cmdSW = StopWatch.create(false);
@@ -260,7 +259,7 @@ export class FileWalker {
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult);
this.addDirectoryEntries(folderQuery, tree, rootFolder, relativeFiles, onResult);
if (last) {
this.matchDirectoryTree(tree, rootFolder, onResult);
@@ -389,13 +388,17 @@ export class FileWalker {
return tree;
}
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
private addDirectoryEntries(folderQuery: IFolderQuery, { pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
// Support relative paths to files from a root resource (ignores excludes)
if (relativeFiles.indexOf(this.filePattern) !== -1) {
this.matchFile(onResult, { base: base, relativePath: this.filePattern });
this.matchFile(onResult, {
base,
relativePath: this.filePattern,
searchPath: this.getSearchPath(folderQuery, this.filePattern)
});
}
function add(relativePath: string) {
const add = (relativePath: string) => {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
let entries = pathToEntries[dirname];
@@ -406,9 +409,10 @@ export class FileWalker {
entries.push({
base,
relativePath,
basename
basename,
searchPath: this.getSearchPath(folderQuery, relativePath),
});
}
};
relativeFiles.forEach(add);
}

View File

@@ -51,7 +51,6 @@ export class DiskSearch implements ISearchResultProvider {
searchDebug: IDebugParams | undefined,
@ILogService private readonly logService: ILogService,
@IConfigurationService private readonly configService: IConfigurationService,
@IFileService private readonly fileService: IFileService
) {
const timeout = this.configService.getValue<ISearchConfiguration>().search.maintainFileSearchCache ?
Number.MAX_VALUE :
@@ -91,41 +90,31 @@ export class DiskSearch implements ISearchResultProvider {
}
textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete> {
const folderQueries = query.folderQueries || [];
return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder)))
.then(exists => {
if (token && token.isCancellationRequested) {
throw canceled();
}
if (token && token.isCancellationRequested) {
throw canceled();
}
query.folderQueries = folderQueries.filter((q, index) => exists[index]);
const event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete> = this.raw.textSearch(query);
const event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete> = this.raw.textSearch(query);
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
});
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
}
fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete> {
const folderQueries = query.folderQueries || [];
return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder)))
.then(exists => {
if (token && token.isCancellationRequested) {
throw canceled();
}
if (token && token.isCancellationRequested) {
throw canceled();
}
query.folderQueries = folderQueries.filter((q, index) => exists[index]);
let event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
event = this.raw.fileSearch(query);
let event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
event = this.raw.fileSearch(query);
const onProgress = (p: ISearchProgressItem) => {
if (!isFileMatch(p)) {
// Should only be for logs
this.logService.debug('SearchService#search', p.message);
}
};
const onProgress = (p: ISearchProgressItem) => {
if (!isFileMatch(p)) {
// Should only be for logs
this.logService.debug('SearchService#search', p.message);
}
};
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
});
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
}
/**

View File

@@ -0,0 +1,111 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import * as path from 'vs/base/common/path';
import { URI } from 'vs/base/common/uri';
import { IFileQuery, IFolderQuery, ISerializedSearchProgressItem, isProgressMessage, QueryType } from 'vs/workbench/services/search/common/search';
import { SearchService } from 'vs/workbench/services/search/node/rawSearchService';
const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures'));
const TEST_FIXTURES2 = path.normalize(getPathFromAmdModule(require, './fixtures2'));
const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples');
const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more');
const TEST_ROOT_FOLDER: IFolderQuery = { folder: URI.file(TEST_FIXTURES) };
const ROOT_FOLDER_QUERY: IFolderQuery[] = [
TEST_ROOT_FOLDER
];
const MULTIROOT_QUERIES: IFolderQuery[] = [
{ folder: URI.file(EXAMPLES_FIXTURES), folderName: 'examples_folder' },
{ folder: URI.file(MORE_FIXTURES) }
];
async function doSearchTest(query: IFileQuery, expectedResultCount: number | Function): Promise<void> {
const svc = new SearchService();
const results: ISerializedSearchProgressItem[] = [];
await svc.doFileSearch(query, e => {
if (!isProgressMessage(e)) {
if (Array.isArray(e)) {
results.push(...e);
} else {
results.push(e);
}
}
});
assert.equal(results.length, expectedResultCount, `rg ${results.length} !== ${expectedResultCount}`);
}
suite('FileSearch-integration', function () {
this.timeout(1000 * 60); // increase timeout for this suite
test('File - simple', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: ROOT_FOLDER_QUERY
};
return doSearchTest(config, 14);
});
test('File - filepattern', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: ROOT_FOLDER_QUERY,
filePattern: 'anotherfile'
};
return doSearchTest(config, 1);
});
test('File - exclude', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: ROOT_FOLDER_QUERY,
filePattern: 'file',
excludePattern: { '**/anotherfolder/**': true }
};
return doSearchTest(config, 2);
});
test('File - multiroot', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: MULTIROOT_QUERIES,
filePattern: 'file',
excludePattern: { '**/anotherfolder/**': true }
};
return doSearchTest(config, 2);
});
test('File - multiroot with folder name', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: MULTIROOT_QUERIES,
filePattern: 'examples_folder anotherfile'
};
return doSearchTest(config, 1);
});
test('File - multiroot with folder name and sibling exclude', () => {
const config: IFileQuery = {
type: QueryType.File,
folderQueries: [
{ folder: URI.file(TEST_FIXTURES), folderName: 'folder1' },
{ folder: URI.file(TEST_FIXTURES2) }
],
filePattern: 'folder1 site',
excludePattern: { '*.css': { when: '$(basename).less' } }
};
return doSearchTest(config, 1);
});
});

View File

@@ -83,6 +83,7 @@ suite('RawSearchService', () => {
const rawMatch: IRawFileMatch = {
base: path.normalize('/some'),
relativePath: 'where',
searchPath: undefined
};
const match: ISerializedFileMatch = {
@@ -232,7 +233,8 @@ suite('RawSearchService', () => {
base: path.normalize('/some/where'),
relativePath,
basename: relativePath,
size: 3
size: 3,
searchPath: undefined
}));
const Engine = TestSearchEngine.bind(null, () => matches.shift()!);
const service = new RawSearchService();
@@ -291,7 +293,8 @@ suite('RawSearchService', () => {
base: path.normalize('/some/where'),
relativePath,
basename: relativePath,
size: 3
size: 3,
searchPath: undefined
}));
const Engine = TestSearchEngine.bind(null, () => matches.shift()!);
const service = new RawSearchService();
@@ -340,6 +343,7 @@ suite('RawSearchService', () => {
matches.push({
base: path.normalize('/some/where'),
relativePath: 'bc',
searchPath: undefined
});
const results: any[] = [];
const cb: IProgressCallback = value => {

View File

@@ -46,7 +46,7 @@ function doSearchTest(query: ITextQuery, expectedResultCount: number | Function)
});
}
suite('Search-integration', function () {
suite('TextSearch-integration', function () {
this.timeout(1000 * 60); // increase timeout for this suite
test('Text: GameOfLife', () => {

View File

@@ -6,11 +6,12 @@
import { Client } from 'vs/base/parts/ipc/common/ipc.net';
import { connect } from 'vs/base/parts/ipc/node/ipc.net';
import { IChannel, IServerChannel, getDelayedChannel } from 'vs/base/parts/ipc/common/ipc';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class SharedProcessService implements ISharedProcessService {
@@ -21,12 +22,13 @@ export class SharedProcessService implements ISharedProcessService {
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IElectronService electronService: IElectronService,
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService
) {
this.sharedProcessMainChannel = mainProcessService.getChannel('sharedProcess');
this.withSharedProcessConnection = this.whenSharedProcessReady()
.then(() => connect(environmentService.sharedIPCHandle, `window:${environmentService.configuration.windowId}`));
.then(() => connect(environmentService.sharedIPCHandle, `window:${electronService.windowId}`));
}
whenSharedProcessReady(): Promise<void> {

View File

@@ -90,6 +90,11 @@ export interface IStatusbarService {
*/
updateEntryVisibility(id: string, visible: boolean): void;
/**
* Focused the status bar. If one of the status bar entries was focused, focuses it directly.
*/
focus(preserveEntryFocus?: boolean): void;
/**
* Focuses the next status bar entry. If none focused, focuses the first.
*/

View File

@@ -36,6 +36,7 @@ export class TelemetryService extends Disposable implements ITelemetryService {
_serviceBrand: undefined;
private impl: ITelemetryService;
public readonly sendErrorTelemetry = false;
constructor(
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,

View File

@@ -24,6 +24,7 @@ export class TelemetryService extends Disposable implements ITelemetryService {
_serviceBrand: undefined;
private impl: ITelemetryService;
public readonly sendErrorTelemetry: boolean;
constructor(
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService,
@@ -48,6 +49,8 @@ export class TelemetryService extends Disposable implements ITelemetryService {
} else {
this.impl = NullTelemetryService;
}
this.sendErrorTelemetry = this.impl.sendErrorTelemetry;
}
setEnabled(value: boolean): void {

View File

@@ -251,11 +251,13 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Next, if the source does not seem to be a file, we try to
// resolve a text model from the resource to get at the
// contents and additional meta data (e.g. encoding).
else if (this.textModelService.hasTextModelContentProvider(source.scheme)) {
else if (this.textModelService.canHandleResource(source)) {
const modelReference = await this.textModelService.createModelReference(source);
success = await this.doSaveAsTextFile(modelReference.object, source, target, options);
modelReference.dispose(); // free up our use of the reference
try {
success = await this.doSaveAsTextFile(modelReference.object, source, target, options);
} finally {
modelReference.dispose(); // free up our use of the reference
}
}
// Finally we simply check if we can find a editor model that

View File

@@ -19,7 +19,6 @@ import { ITextBufferFactory, ITextModel } from 'vs/editor/common/model';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILogService } from 'vs/platform/log/common/log';
import { basename } from 'vs/base/common/path';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IWorkingCopyService, IWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { ILabelService } from 'vs/platform/label/common/label';
@@ -680,7 +679,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// - the model is not in orphan mode (because in that case we know the file does not exist on disk)
// - the model version did not change due to save participants running
if (options.force && !this.dirty && !this.inOrphanMode && options.reason === SaveReason.EXPLICIT && versionId === this.versionId) {
return this.doTouch(this.versionId, options.reason);
return this.doTouch(this.versionId, options);
}
// update versionId with its new value (if pre-save changes happened)
@@ -755,11 +754,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this._onDidSaveError.fire();
}
private doTouch(this: TextFileEditorModel & IResolvedTextFileEditorModel, versionId: number, reason: SaveReason): Promise<void> {
private doTouch(this: TextFileEditorModel & IResolvedTextFileEditorModel, versionId: number, options: ITextFileSaveOptions): Promise<void> {
const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat);
return this.saveSequentializer.setPending(versionId, (async () => {
try {
// Write contents to touch: we used to simply update the mtime of the file
// but this lead to weird results, either for external watchers or even for
// us where we thought the file has changed on disk. As such, we let the OS
// handle the increment of mtime and not deal with it ourselves.
const stat = await this.textFileService.write(lastResolvedFileStat.resource, this.createSnapshot(), {
mtime: lastResolvedFileStat.mtime,
encoding: this.getEncoding(),
@@ -770,9 +774,17 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.updateLastResolvedFileStat(stat);
// Emit File Saved Event
this._onDidSave.fire(reason);
this._onDidSave.fire(options.reason ?? SaveReason.EXPLICIT);
} catch (error) {
onUnexpectedError(error); // just log any error but do not notify the user since the file was not dirty
// In any case of an error, we mark the model as dirty to prevent data loss
// It could be possible that the touch corrupted the file on disk (e.g. when
// an error happened after truncating the file) and as such we want to preserve
// the model contents to prevent data loss
this.setDirty(true);
// Notify user to handle this save error
this.handleSaveError(error, versionId, options);
}
})());
}

View File

@@ -23,7 +23,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IWorkingCopyFileService, WorkingCopyFileEvent } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { ITextSnapshot, ITextBufferFactory } from 'vs/editor/common/model';
import { joinPath, isEqualOrParent, isEqual } from 'vs/base/common/resources';
import { joinPath, isEqualOrParent, isEqual, extUri } from 'vs/base/common/resources';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
@@ -142,7 +142,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
for (const model of this.models) {
const resource = model.resource;
if (isEqualOrParent(resource, e.target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) {
if (extUri.isEqualOrParent(resource, e.target/* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) {
targetModels.push(model);
}

View File

@@ -146,6 +146,40 @@ suite('Files - TextFileEditorModel', () => {
assert.ok(!accessor.modelService.getModel(model.resource));
});
test('save - touching with error turns model dirty', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);
await model.load();
let saveErrorEvent = false;
model.onDidSaveError(() => saveErrorEvent = true);
let savedEvent = false;
model.onDidSave(() => savedEvent = true);
accessor.fileService.writeShouldThrowError = new Error('failed to write');
try {
await model.save({ force: true });
assert.ok(model.hasState(TextFileEditorModelState.ERROR));
assert.ok(model.isDirty());
assert.ok(saveErrorEvent);
assert.equal(accessor.workingCopyService.dirtyCount, 1);
assert.equal(accessor.workingCopyService.isDirty(model.resource), true);
} finally {
accessor.fileService.writeShouldThrowError = undefined;
}
await model.save({ force: true });
assert.ok(savedEvent);
assert.ok(!model.isDirty());
model.dispose();
assert.ok(!accessor.modelService.getModel(model.resource));
});
test('save error (generic)', async function () {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/index_async.txt'), 'utf8', undefined);

View File

@@ -6,38 +6,56 @@
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITextModel } from 'vs/editor/common/model';
import { IDisposable, toDisposable, IReference, ReferenceCollection, ImmortalReference } from 'vs/base/common/lifecycle';
import { IDisposable, toDisposable, IReference, ReferenceCollection, Disposable } from 'vs/base/common/lifecycle';
import { IModelService } from 'vs/editor/common/services/modelService';
import { ResourceEditorModel } from 'vs/workbench/common/editor/resourceEditorModel';
import { ITextFileService, TextFileLoadReason } from 'vs/workbench/services/textfile/common/textfiles';
import * as network from 'vs/base/common/network';
import { ITextModelService, ITextModelContentProvider, ITextEditorModel, IResolvedTextEditorModel } from 'vs/editor/common/services/resolverService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
import { ModelUndoRedoParticipant } from 'vs/editor/common/services/modelUndoRedoParticipant';
class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorModel>> {
private providers: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null);
private modelsToDispose = new Set<string>();
private readonly providers: { [scheme: string]: ITextModelContentProvider[] } = Object.create(null);
private readonly modelsToDispose = new Set<string>();
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ITextFileService private readonly textFileService: ITextFileService,
@IFileService private readonly fileService: IFileService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IModelService private readonly modelService: IModelService
) {
super();
}
async createReferencedObject(key: string, skipActivateProvider?: boolean): Promise<ITextEditorModel> {
// Untrack as being disposed
this.modelsToDispose.delete(key);
// inMemory Schema: go through model service cache
const resource = URI.parse(key);
if (resource.scheme === network.Schemas.inMemory) {
const cachedModel = this.modelService.getModel(resource);
if (!cachedModel) {
throw new Error(`Unable to resolve inMemory resource ${key}`);
}
// File or remote file provider already known
return this.instantiationService.createInstance(ResourceEditorModel, resource);
}
// Untitled Schema: go through untitled text service
if (resource.scheme === network.Schemas.untitled) {
return this.textFileService.untitled.resolve({ untitledResource: resource });
}
// File or remote file: go through text file service
if (this.fileService.canHandleResource(resource)) {
return this.textFileService.files.resolve(resource, { reason: TextFileLoadReason.REFERENCE });
}
@@ -56,19 +74,26 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
return this.createReferencedObject(key, true);
}
throw new Error('resource is not available');
throw new Error(`Unable to resolve resource ${key}`);
}
destroyReferencedObject(key: string, modelPromise: Promise<ITextEditorModel>): void {
// Track as being disposed
this.modelsToDispose.add(key);
modelPromise.then(model => {
if (this.modelsToDispose.has(key)) {
if (model instanceof TextFileEditorModel) {
this.textFileService.files.disposeModel(model);
} else {
model.dispose();
}
if (!this.modelsToDispose.has(key)) {
return; // return if model has been aquired again meanwhile
}
const resource = URI.parse(key);
if (resource.scheme === network.Schemas.untitled || resource.scheme === network.Schemas.inMemory) {
// untitled and inMemory are bound to a different lifecycle
} else if (model instanceof TextFileEditorModel) {
this.textFileService.files.disposeModel(model);
} else {
model.dispose();
}
}, err => {
// ignore
@@ -131,53 +156,37 @@ class ResourceModelCollection extends ReferenceCollection<Promise<ITextEditorMod
return value;
}
}
throw new Error('resource is not available');
throw new Error(`Unable to resolve text model content for resource ${key}`);
}
}
export class TextModelResolverService implements ITextModelService {
export class TextModelResolverService extends Disposable implements ITextModelService {
_serviceBrand: undefined;
private resourceModelCollection: ResourceModelCollection;
private readonly resourceModelCollection = this.instantiationService.createInstance(ResourceModelCollection);
constructor(
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IUndoRedoService private readonly undoRedoService: IUndoRedoService,
@IModelService private readonly modelService: IModelService
) {
this.resourceModelCollection = instantiationService.createInstance(ResourceModelCollection);
super();
this._register(new ModelUndoRedoParticipant(this.modelService, this, this.undoRedoService));
}
createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
return this.doCreateModelReference(resource);
}
private async doCreateModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
// Untitled Schema: go through untitled text service
if (resource.scheme === network.Schemas.untitled) {
const model = await this.untitledTextEditorService.resolve({ untitledResource: resource });
return new ImmortalReference(model);
}
// InMemory Schema: go through model service cache
if (resource.scheme === network.Schemas.inMemory) {
const cachedModel = this.modelService.getModel(resource);
if (!cachedModel) {
throw new Error('Cant resolve inmemory resource');
}
return new ImmortalReference(this.instantiationService.createInstance(ResourceEditorModel, resource) as IResolvedTextEditorModel);
}
async createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
const ref = this.resourceModelCollection.acquire(resource.toString());
try {
const model = await ref.object;
return { object: model as IResolvedTextEditorModel, dispose: () => ref.dispose() };
return {
object: model as IResolvedTextEditorModel,
dispose: () => ref.dispose()
};
} catch (error) {
ref.dispose();
@@ -189,12 +198,12 @@ export class TextModelResolverService implements ITextModelService {
return this.resourceModelCollection.registerTextModelContentProvider(scheme, provider);
}
hasTextModelContentProvider(scheme: string): boolean {
if (scheme === network.Schemas.untitled || scheme === network.Schemas.inMemory) {
return true; // we handle untitled:// and inMemory:// within
canHandleResource(resource: URI): boolean {
if (this.fileService.canHandleResource(resource) || resource.scheme === network.Schemas.untitled || resource.scheme === network.Schemas.inMemory) {
return true; // we handle file://, untitled:// and inMemory:// automatically
}
return this.resourceModelCollection.hasTextModelContentProvider(scheme);
return this.resourceModelCollection.hasTextModelContentProvider(resource.scheme);
}
}

View File

@@ -103,8 +103,9 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
@IWorkbenchLayoutService readonly layoutService: IWorkbenchLayoutService,
@ILogService private readonly logService: ILogService
) {
this.container = layoutService.getWorkbenchContainer();
this.settings = new ThemeConfiguration(configurationService);
this.container = layoutService.container;
const defaultThemeType = environmentService.configuration.defaultThemeType || DARK;
this.settings = new ThemeConfiguration(configurationService, defaultThemeType);
this.colorThemeRegistry = new ThemeRegistry(extensionService, colorThemesExtPoint, ColorThemeData.fromExtensionTheme);
this.colorThemeWatcher = new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this));
@@ -125,9 +126,11 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
// themes are loaded asynchronously, we need to initialize
// a color theme document with good defaults until the theme is loaded
let themeData: ColorThemeData | undefined = ColorThemeData.fromStorageData(this.storageService);
const containerBaseTheme = this.getBaseThemeFromContainer();
if (!themeData || themeData.baseTheme !== containerBaseTheme) {
themeData = ColorThemeData.createUnloadedTheme(containerBaseTheme);
if (environmentService.configuration.highContrast && themeData?.baseTheme !== HIGH_CONTRAST) {
themeData = ColorThemeData.createUnloadedThemeForThemeType(HIGH_CONTRAST);
}
if (!themeData) {
themeData = ColorThemeData.createUnloadedThemeForThemeType(defaultThemeType);
}
themeData.setCustomizations(this.settings);
this.applyTheme(themeData, undefined, true);
@@ -619,16 +622,6 @@ export class WorkbenchThemeService implements IWorkbenchThemeService {
this.onProductIconThemeChange.fire(this.currentProductIconTheme);
}
private getBaseThemeFromContainer() {
for (let i = this.container.classList.length - 1; i >= 0; i--) {
const item = this.container.classList.item(i);
if (item === VS_LIGHT_THEME || item === VS_DARK_THEME || item === VS_HC_THEME) {
return item;
}
}
return VS_DARK_THEME;
}
}
class ThemeFileWatcher {

View File

@@ -32,7 +32,7 @@ const configurationExtPoint = ExtensionsRegistry.registerExtensionPoint<IColorEx
type: 'string',
description: nls.localize('contributes.color.id', 'The identifier of the themable color'),
pattern: colorIdPattern,
patternErrorMessage: nls.localize('contributes.color.id.format', 'Identifiers should be in the form aa[.bb]*'),
patternErrorMessage: nls.localize('contributes.color.id.format', 'Identifiers must only contain letters, digits and dots and can not start with a dot'),
},
description: {
type: 'string',
@@ -102,7 +102,7 @@ export class ColorExtensionPoint {
return;
}
if (!colorContribution.id.match(colorIdPattern)) {
collector.error(nls.localize('invalid.id.format', "'configuration.colors.id' must follow the word[.word]*"));
collector.error(nls.localize('invalid.id.format', "'configuration.colors.id' must only contain letters, digits and dots and can not start with a dot"));
return;
}
if (typeof colorContribution.description !== 'string' || colorContribution.id.length === 0) {

View File

@@ -14,7 +14,7 @@ import * as objects from 'vs/base/common/objects';
import * as arrays from 'vs/base/common/arrays';
import * as resources from 'vs/base/common/resources';
import { Extensions as ColorRegistryExtensions, IColorRegistry, ColorIdentifier, editorBackground, editorForeground } from 'vs/platform/theme/common/colorRegistry';
import { ThemeType, ITokenStyle } from 'vs/platform/theme/common/themeService';
import { ThemeType, ITokenStyle, getThemeTypeSelector } from 'vs/platform/theme/common/themeService';
import { Registry } from 'vs/platform/registry/common/platform';
import { getParseErrorMessage } from 'vs/base/common/jsonErrorMessages';
import { URI } from 'vs/base/common/uri';
@@ -550,6 +550,10 @@ export class ColorThemeData implements IWorkbenchColorTheme {
// constructors
static createUnloadedThemeForThemeType(themeType: ThemeType): ColorThemeData {
return ColorThemeData.createUnloadedTheme(getThemeTypeSelector(themeType));
}
static createUnloadedTheme(id: string): ColorThemeData {
let themeData = new ColorThemeData(id, '', '__' + id);
themeData.isLoaded = false;

View File

@@ -14,8 +14,8 @@ import { workbenchColorsSchemaId } from 'vs/platform/theme/common/colorRegistry'
import { tokenStylingSchemaId } from 'vs/platform/theme/common/tokenClassificationRegistry';
import { ThemeSettings, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IColorCustomizations, ITokenColorCustomizations, IWorkbenchProductIconTheme, ISemanticTokenColorCustomizations, IExperimentalSemanticTokenColorCustomizations } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { ThemeType, HIGH_CONTRAST, LIGHT } from 'vs/platform/theme/common/themeService';
const DEFAULT_THEME_SETTING_VALUE = 'Default Light Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme
const DEFAULT_THEME_DARK_SETTING_VALUE = 'Default Dark Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme
const DEFAULT_THEME_LIGHT_SETTING_VALUE = 'Default Light Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme
const DEFAULT_THEME_HC_SETTING_VALUE = 'Default High Contrast Azure Data Studio'; // {{SQL CARBON EDIT}} replace default theme
@@ -33,7 +33,7 @@ const colorThemeSettingEnumDescriptions: string[] = [];
const colorThemeSettingSchema: IConfigurationPropertySchema = {
type: 'string',
description: nls.localize('colorTheme', "Specifies the color theme used in the workbench."),
default: DEFAULT_THEME_SETTING_VALUE,
default: DEFAULT_THEME_DARK_SETTING_VALUE,
enum: colorThemeSettingEnum,
enumDescriptions: colorThemeSettingEnumDescriptions,
errorMessage: nls.localize('colorThemeError', "Theme is unknown or not installed."),
@@ -110,7 +110,6 @@ const themeSettingsConfiguration: IConfigurationNode = {
[ThemeSettings.PRODUCT_ICON_THEME]: productIconThemeSettingSchema
}
};
configurationRegistry.registerConfiguration(themeSettingsConfiguration);
function tokenGroupSettings(description: string): IJSONSchema {
return {
@@ -232,7 +231,19 @@ export function updateProductIconThemeConfigurationSchemas(themes: IWorkbenchPro
export class ThemeConfiguration {
constructor(private configurationService: IConfigurationService) {
constructor(private configurationService: IConfigurationService, themeType: ThemeType) {
switch (themeType) {
case LIGHT:
colorThemeSettingSchema.default = DEFAULT_THEME_LIGHT_SETTING_VALUE;
break;
case HIGH_CONTRAST:
colorThemeSettingSchema.default = DEFAULT_THEME_HC_SETTING_VALUE;
break;
default:
colorThemeSettingSchema.default = DEFAULT_THEME_DARK_SETTING_VALUE;
break;
}
configurationRegistry.registerConfiguration(themeSettingsConfiguration);
}
public get colorTheme(): string {

View File

@@ -15,7 +15,7 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { ExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/electron-browser/extensionResourceLoaderService';
import { ExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/electron-sandbox/extensionResourceLoaderService';
import { ITokenStyle } from 'vs/platform/theme/common/themeService';
const undefinedStyle = { bold: undefined, underline: undefined, italic: undefined };

View File

@@ -7,7 +7,7 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { virtualMachineHint } from 'vs/base/node/id';
import * as perf from 'vs/base/common/performance';
import * as os from 'os';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { TitlebarPart } from 'vs/workbench/electron-sandbox/parts/titlebar/titlebarPart';
import { ITitleService } from 'vs/workbench/services/title/common/titleService';
registerSingleton(ITitleService, TitlebarPart);

View File

@@ -3,7 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport, TextResourceEditorInput } from 'vs/workbench/common/editor';
import { IEncodingSupport, EncodingMode, Verbosity, IModeSupport } from 'vs/workbench/common/editor';
import { AbstractTextResourceEditorInput } from 'vs/workbench/common/editor/textResourceEditorInput';
import { IUntitledTextEditorModel } from 'vs/workbench/services/untitled/common/untitledTextEditorModel';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ILabelService } from 'vs/platform/label/common/label';
@@ -12,11 +13,12 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IFileService } from 'vs/platform/files/common/files';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { basenameOrAuthority } from 'vs/base/common/resources';
/**
* An editor input to be used for untitled text buffers.
*/
export class UntitledTextEditorInput extends TextResourceEditorInput implements IEncodingSupport, IModeSupport {
export class UntitledTextEditorInput extends AbstractTextResourceEditorInput implements IEncodingSupport, IModeSupport {
static readonly ID: string = 'workbench.editors.untitledEditorInput';
@@ -50,6 +52,10 @@ export class UntitledTextEditorInput extends TextResourceEditorInput implements
return UntitledTextEditorInput.ID;
}
get ariaLabel(): string {
return basenameOrAuthority(this.resource);
}
getName(): string {
return this.model.name;
}

View File

@@ -40,12 +40,12 @@ export interface IUntitledTextEditorModel extends ITextEditorModel, IModeSupport
readonly onDidRevert: Event<void>;
/**
* Wether this untitled text model has an associated file path.
* Whether this untitled text model has an associated file path.
*/
readonly hasAssociatedFilePath: boolean;
/**
* Wether this model has an explicit language mode or not.
* Whether this model has an explicit language mode or not.
*/
readonly hasModeSetExplicitly: boolean;

View File

@@ -6,7 +6,7 @@
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { Event, Emitter } from 'vs/base/common/event';
import { IUpdateService, State } from 'vs/platform/update/common/update';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class NativeUpdateService implements IUpdateService {

View File

@@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IExtUri } from 'vs/base/common/resources';
export interface IUriIdentity {
readonly pathHierarchical: boolean;
readonly ignorePathCasing: boolean;
}
export const IUriIdentityService = createDecorator<IUriIdentityService>('IUriIdentityService');
export interface IUriIdentityService {
_serviceBrand: undefined;
/**
* Uri extensions that are aware of casing.
*/
readonly extUri: IExtUri;
/**
* Returns a canonical uri for the given resource. Different uris can point to the same
* resource. That's because of casing or missing normalization, e.g the following uris
* are different but refer to the same document (because windows paths are not case-sensitive)
*
* ```txt
* file:///c:/foo/bar.txt
* file:///c:/FOO/BAR.txt
* ```
*
* This function should be invoked when feeding uris into the system that represent the truth,
* e.g document uris or marker-to-document associations etc. This function should NOT be called
* to pretty print a label nor to sanitize a uri.
*
* Samples:
*
* | in | out | |
* |---|---|---|
* | `file:///foo/bar/../bar` | `file:///foo/bar` | n/a |
* | `file:///foo/bar/../bar#frag` | `file:///foo/bar#frag` | keep fragment |
* | `file:///foo/BAR` | `file:///foo/bar` | assume ignore case |
* | `file:///foo/bar/../BAR?q=2` | `file:///foo/BAR?q=2` | query makes it a different document |
*/
asCanonicalUri(uri: URI): URI;
}

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity';
import { URI } from 'vs/base/common/uri';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { ExtUri, IExtUri, normalizePath } from 'vs/base/common/resources';
import { SkipList } from 'vs/base/common/skipList';
class Entry {
static _clock = 0;
time: number = Entry._clock++;
constructor(readonly uri: URI) { }
touch() {
this.time = Entry._clock++;
return this;
}
}
export class UriIdentityService implements IUriIdentityService {
_serviceBrand: undefined;
readonly extUri: IExtUri;
private readonly _canonicalUris: SkipList<URI, Entry>;
private readonly _limit = 2 ** 16;
constructor(@IFileService private readonly _fileService: IFileService) {
// assume path casing matters unless the file system provider spec'ed the opposite
const ignorePathCasing = (uri: URI): boolean => {
// perf@jrieken cache this information
if (this._fileService.canHandleResource(uri)) {
return !this._fileService.hasCapability(uri, FileSystemProviderCapabilities.PathCaseSensitive);
}
// this defaults to false which is a good default for
// * virtual documents
// * in-memory uris
// * all kind of "private" schemes
return false;
};
this.extUri = new ExtUri(ignorePathCasing);
this._canonicalUris = new SkipList((a, b) => this.extUri.compare(a, b, true), this._limit);
}
asCanonicalUri(uri: URI): URI {
// todo@jrieken there is more to it than just comparing
// * ASYNC!?
// * windows 8.3-filenames
// * substr-drives...
// * sym links?
// * fetch real casing?
// (1) normalize URI
if (this._fileService.canHandleResource(uri)) {
uri = normalizePath(uri);
}
// (2) find the uri in its canonical form or use this uri to define it
let item = this._canonicalUris.get(uri);
if (item) {
return item.touch().uri.with({ fragment: uri.fragment });
}
// this uri is first and defines the canonical form
this._canonicalUris.set(uri, new Entry(uri));
this._checkTrim();
return uri;
}
private _checkTrim(): void {
if (this._canonicalUris.size < this._limit) {
return;
}
// get all entries, sort by touch (MRU) and re-initalize
// the uri cache and the entry clock. this is an expensive
// operation and should happen rarely
const entries = [...this._canonicalUris.entries()].sort((a, b) => {
if (a[1].touch < b[1].touch) {
return 1;
} else if (a[1].touch > b[1].touch) {
return -1;
} else {
return 0;
}
});
Entry._clock = 0;
this._canonicalUris.clear();
const newSize = this._limit * 0.5;
for (let i = 0; i < newSize; i++) {
this._canonicalUris.set(entries[i][0], entries[i][1].touch());
}
}
}
registerSingleton(IUriIdentityService, UriIdentityService, true);

View File

@@ -0,0 +1,94 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { UriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentityService';
import { mock } from 'vs/workbench/test/common/workbenchTestServices';
import { IFileService, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
suite('URI Identity', function () {
class FakeFileService extends mock<IFileService>() {
constructor(readonly data: Map<string, FileSystemProviderCapabilities>) {
super();
}
canHandleResource(uri: URI) {
return this.data.has(uri.scheme);
}
hasCapability(uri: URI, flag: FileSystemProviderCapabilities): boolean {
const mask = this.data.get(uri.scheme) ?? 0;
return Boolean(mask & flag);
}
}
let _service: UriIdentityService;
setup(function () {
_service = new UriIdentityService(new FakeFileService(new Map([
['bar', FileSystemProviderCapabilities.PathCaseSensitive],
['foo', 0]
])));
});
function assertCanonical(input: URI, expected: URI, service: UriIdentityService = _service) {
const actual = service.asCanonicalUri(input);
assert.equal(actual.toString(), expected.toString());
assert.ok(service.extUri.isEqual(actual, expected));
}
test('extUri (isEqual)', function () {
let a = URI.parse('foo://bar/bang');
let a1 = URI.parse('foo://bar/BANG');
let b = URI.parse('bar://bar/bang');
let b1 = URI.parse('bar://bar/BANG');
assert.equal(_service.extUri.isEqual(a, a1), true);
assert.equal(_service.extUri.isEqual(a1, a), true);
assert.equal(_service.extUri.isEqual(b, b1), false);
assert.equal(_service.extUri.isEqual(b1, b), false);
});
test('asCanonicalUri (casing)', function () {
let a = URI.parse('foo://bar/bang');
let a1 = URI.parse('foo://bar/BANG');
let b = URI.parse('bar://bar/bang');
let b1 = URI.parse('bar://bar/BANG');
assertCanonical(a, a);
assertCanonical(a1, a);
assertCanonical(b, b);
assertCanonical(b1, b1); // case sensitive
});
test('asCanonicalUri (normalization)', function () {
let a = URI.parse('foo://bar/bang');
assertCanonical(a, a);
assertCanonical(URI.parse('foo://bar/./bang'), a);
assertCanonical(URI.parse('foo://bar/./bang'), a);
assertCanonical(URI.parse('foo://bar/./foo/../bang'), a);
});
test('asCanonicalUri (keep fragement)', function () {
let a = URI.parse('foo://bar/bang');
assertCanonical(a, a);
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./bang#frag'), a.with({ fragment: 'frag' }));
assertCanonical(URI.parse('foo://bar/./foo/../bang#frag'), a.with({ fragment: 'frag' }));
let b = URI.parse('foo://bar/bazz#frag');
assertCanonical(b, b);
assertCanonical(URI.parse('foo://bar/bazz'), b.with({ fragment: '' }));
assertCanonical(URI.parse('foo://bar/BAZZ#DDD'), b.with({ fragment: 'DDD' })); // lower-case path, but fragment is kept
});
});

View File

@@ -5,30 +5,27 @@
import { IURLService, IURLHandler, IOpenURLOptions } from 'vs/platform/url/common/url';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { URLHandlerChannel } from 'vs/platform/url/common/urlIpc';
import { URLService } from 'vs/platform/url/node/urlService';
import { IOpenerService, IOpener, matchesScheme } from 'vs/platform/opener/common/opener';
import product from 'vs/platform/product/common/product';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { NativeURLService } from 'vs/platform/url/common/urlService';
export interface IRelayOpenURLOptions extends IOpenURLOptions {
openToSide?: boolean;
openExternal?: boolean;
}
export class RelayURLService extends URLService implements IURLHandler, IOpener {
export class RelayURLService extends NativeURLService implements IURLHandler, IOpener {
private urlService: IURLService;
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IOpenerService openerService: IOpenerService,
@IWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService,
@IElectronService private electronService: IElectronService
) {
super();
@@ -44,9 +41,9 @@ export class RelayURLService extends URLService implements IURLHandler, IOpener
let query = uri.query;
if (!query) {
query = `windowId=${encodeURIComponent(this.environmentService.configuration.windowId)}`;
query = `windowId=${encodeURIComponent(this.electronService.windowId)}`;
} else {
query += `&windowId=${encodeURIComponent(this.environmentService.configuration.windowId)}`;
query += `&windowId=${encodeURIComponent(this.electronService.windowId)}`;
}
return uri.with({ query });

View File

@@ -16,7 +16,7 @@ import { URI } from 'vs/base/common/uri';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { joinPath, dirname } from 'vs/base/common/resources';
import { VSBuffer } from 'vs/base/common/buffer';
import { DiskFileSystemProvider } from 'vs/platform/files/electron-browser/diskFileSystemProvider';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { BACKUPS } from 'vs/platform/environment/common/environment';
import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { BrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';

View File

@@ -0,0 +1,426 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IUserDataSyncService, IUserDataSyncEnablementService, IAuthenticationProvider, getUserDataSyncStore, isAuthenticationProvider } from 'vs/platform/userDataSync/common/userDataSync';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IUserDataSyncWorkbenchService, IUserDataSyncAccount, AccountStatus, CONTEXT_SYNC_ENABLEMENT, CONTEXT_SYNC_STATE, CONTEXT_ACCOUNT_STATE } from 'vs/workbench/services/userDataSync/common/userDataSync';
import { AuthenticationSession, AuthenticationSessionsChangeEvent } from 'vs/editor/common/modes';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { flatten } from 'vs/base/common/arrays';
import { values } from 'vs/base/common/map';
import { IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { IAuthenticationTokenService } from 'vs/platform/authentication/common/authentication';
import { IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { localize } from 'vs/nls';
import { canceled } from 'vs/base/common/errors';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
type UserAccountClassification = {
id: { classification: 'EndUserPseudonymizedInformation', purpose: 'BusinessInsight' };
};
type FirstTimeSyncClassification = {
action: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
};
type UserAccountEvent = {
id: string;
};
type AccountQuickPickItem = { label: string, authenticationProvider: IAuthenticationProvider, account?: UserDataSyncAccount, description?: string };
class UserDataSyncAccount implements IUserDataSyncAccount {
constructor(readonly authenticationProviderId: string, private readonly session: AuthenticationSession) { }
get sessionId(): string { return this.session.id; }
get accountName(): string { return this.session.account.displayName; }
get accountId(): string { return this.session.account.id; }
get token(): string { return this.session.accessToken; }
}
export class UserDataSyncWorkbenchService extends Disposable implements IUserDataSyncWorkbenchService {
_serviceBrand: any;
private static DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY = 'userDataSyncAccount.donotUseWorkbenchSession';
private static CACHED_SESSION_STORAGE_KEY = 'userDataSyncAccountPreference';
readonly authenticationProviders: IAuthenticationProvider[];
private _accountStatus: AccountStatus = AccountStatus.Uninitialized;
get accountStatus(): AccountStatus { return this._accountStatus; }
private readonly _onDidChangeAccountStatus = this._register(new Emitter<AccountStatus>());
readonly onDidChangeAccountStatus = this._onDidChangeAccountStatus.event;
private _all: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>();
get all(): UserDataSyncAccount[] { return flatten(values(this._all)); }
get current(): UserDataSyncAccount | undefined { return this.all.filter(account => this.isCurrentAccount(account))[0]; }
private readonly syncEnablementContext: IContextKey<boolean>;
private readonly syncStatusContext: IContextKey<string>;
private readonly accountStatusContext: IContextKey<string>;
constructor(
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IAuthenticationTokenService private readonly authenticationTokenService: IAuthenticationTokenService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IStorageService private readonly storageService: IStorageService,
@IUserDataSyncEnablementService private readonly userDataSyncEnablementService: IUserDataSyncEnablementService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@ILogService private readonly logService: ILogService,
@IProductService productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IExtensionService extensionService: IExtensionService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@INotificationService private readonly notificationService: INotificationService,
@IDialogService private readonly dialogService: IDialogService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super();
this.authenticationProviders = getUserDataSyncStore(productService, configurationService)?.authenticationProviders || [];
this.syncEnablementContext = CONTEXT_SYNC_ENABLEMENT.bindTo(contextKeyService);
this.syncStatusContext = CONTEXT_SYNC_STATE.bindTo(contextKeyService);
this.accountStatusContext = CONTEXT_ACCOUNT_STATE.bindTo(contextKeyService);
if (this.authenticationProviders.length) {
this.syncStatusContext.set(this.userDataSyncService.status);
this._register(userDataSyncService.onDidChangeStatus(status => this.syncStatusContext.set(status)));
this.syncEnablementContext.set(this.userDataSyncEnablementService.isEnabled());
this._register(this.userDataSyncEnablementService.onDidChangeEnablement(enabled => this.syncEnablementContext.set(enabled)));
extensionService.whenInstalledExtensionsRegistered().then(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
this.initialize();
} else {
const disposable = this.authenticationService.onDidRegisterAuthenticationProvider(() => {
if (this.authenticationProviders.every(({ id }) => authenticationService.isAuthenticationProviderRegistered(id))) {
disposable.dispose();
this.initialize();
}
});
}
});
}
}
private async initialize(): Promise<void> {
if (this.currentSessionId === undefined && this.useWorkbenchSessionId && this.environmentService.options?.authenticationSessionId) {
this.currentSessionId = this.environmentService.options.authenticationSessionId;
this.useWorkbenchSessionId = false;
}
await this.update();
this._register(
Event.any(
Event.filter(
Event.any(
this.authenticationService.onDidRegisterAuthenticationProvider,
this.authenticationService.onDidUnregisterAuthenticationProvider,
), authenticationProviderId => this.isSupportedAuthenticationProviderId(authenticationProviderId)),
this.authenticationTokenService.onTokenFailed)
(() => this.update()));
this._register(Event.filter(this.authenticationService.onDidChangeSessions, e => this.isSupportedAuthenticationProviderId(e.providerId))(({ event }) => this.onDidChangeSessions(event)));
this._register(this.storageService.onDidChangeStorage(e => this.onDidChangeStorage(e)));
}
private async update(): Promise<void> {
const allAccounts: Map<string, UserDataSyncAccount[]> = new Map<string, UserDataSyncAccount[]>();
for (const { id } of this.authenticationProviders) {
const accounts = await this.getAccounts(id);
allAccounts.set(id, accounts);
}
this._all = allAccounts;
const current = this.current;
await this.updateToken(current);
this.updateAccountStatus(current);
}
private async getAccounts(authenticationProviderId: string): Promise<UserDataSyncAccount[]> {
let accounts: Map<string, UserDataSyncAccount> = new Map<string, UserDataSyncAccount>();
let currentAccount: UserDataSyncAccount | null = null;
const sessions = await this.authenticationService.getSessions(authenticationProviderId) || [];
for (const session of sessions) {
const account: UserDataSyncAccount = new UserDataSyncAccount(authenticationProviderId, session);
accounts.set(account.accountName, account);
if (this.isCurrentAccount(account)) {
currentAccount = account;
}
}
if (currentAccount) {
// Always use current account if available
accounts.set(currentAccount.accountName, currentAccount);
}
return values(accounts);
}
private async updateToken(current: UserDataSyncAccount | undefined): Promise<void> {
let value: { token: string, authenticationProviderId: string } | undefined = undefined;
if (current) {
try {
this.logService.trace('Preferences Sync: Updating the token for the account', current.accountName);
const token = current.token;
this.logService.trace('Preferences Sync: Token updated for the account', current.accountName);
value = { token, authenticationProviderId: current.authenticationProviderId };
} catch (e) {
this.logService.error(e);
}
}
await this.authenticationTokenService.setToken(value);
}
private updateAccountStatus(current: UserDataSyncAccount | undefined): void {
// set status
const accountStatus: AccountStatus = current ? AccountStatus.Available : AccountStatus.Unavailable;
if (this._accountStatus !== accountStatus) {
const previous = this._accountStatus;
this.logService.debug('Sync account status changed', previous, accountStatus);
if (previous === AccountStatus.Available && accountStatus === AccountStatus.Unavailable) {
this.turnoff(false);
}
this._accountStatus = accountStatus;
this.accountStatusContext.set(accountStatus);
this._onDidChangeAccountStatus.fire(accountStatus);
}
}
async turnOn(): Promise<void> {
const picked = await this.pick();
if (!picked) {
throw canceled();
}
// User did not pick an account or login failed
if (this.accountStatus !== AccountStatus.Available) {
throw new Error(localize('no account', "No account available"));
}
await this.handleFirstTimeSync();
this.userDataSyncEnablementService.setEnablement(true);
this.notificationService.info(localize('sync turned on', "Preferences sync is turned on"));
}
async turnoff(everywhere: boolean): Promise<void> {
if (everywhere) {
this.telemetryService.publicLog2('sync/turnOffEveryWhere');
await this.userDataSyncService.reset();
} else {
await this.userDataSyncService.resetLocal();
}
this.userDataSyncEnablementService.setEnablement(false);
}
private async handleFirstTimeSync(): Promise<void> {
const isFirstSyncWithMerge = await this.userDataSyncService.isFirstTimeSyncWithMerge();
if (!isFirstSyncWithMerge) {
return;
}
const result = await this.dialogService.show(
Severity.Info,
localize('firs time sync', "Sync"),
[
localize('merge', "Merge"),
localize('cancel', "Cancel"),
localize('replace', "Replace Local"),
],
{
cancelId: 1,
detail: localize('first time sync detail', "It looks like this is the first time sync is set up.\nWould you like to merge or replace with the data from the cloud?"),
}
);
switch (result.choice) {
case 0:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'merge' });
break;
case 1:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'cancelled' });
throw canceled();
case 2:
this.telemetryService.publicLog2<{ action: string }, FirstTimeSyncClassification>('sync/firstTimeSync', { action: 'replace-local' });
await this.userDataSyncService.pull();
break;
}
}
private isSupportedAuthenticationProviderId(authenticationProviderId: string): boolean {
return this.authenticationProviders.some(({ id }) => id === authenticationProviderId);
}
private isCurrentAccount(account: UserDataSyncAccount): boolean {
return account.sessionId === this.currentSessionId;
}
async pickAccount(): Promise<void> {
await this.pick();
}
private async pick(): Promise<boolean> {
const result = await this.doPick();
if (!result) {
return false;
}
let sessionId: string, accountName: string, accountId: string;
if (isAuthenticationProvider(result)) {
const session = await this.authenticationService.login(result.id, result.scopes);
sessionId = session.id;
accountName = session.account.displayName;
accountId = session.account.id;
} else {
sessionId = result.sessionId;
accountName = result.accountName;
accountId = result.accountId;
}
await this.switch(sessionId, accountName, accountId);
return true;
}
private async doPick(): Promise<UserDataSyncAccount | IAuthenticationProvider | undefined> {
if (this.authenticationProviders.length === 0) {
return undefined;
}
await this.update();
// Single auth provider and no accounts available
if (this.authenticationProviders.length === 1 && !this.all.length) {
return this.authenticationProviders[0];
}
return new Promise<UserDataSyncAccount | IAuthenticationProvider | undefined>(async (c, e) => {
let result: UserDataSyncAccount | IAuthenticationProvider | undefined;
const disposables: DisposableStore = new DisposableStore();
const quickPick = this.quickInputService.createQuickPick<AccountQuickPickItem>();
disposables.add(quickPick);
quickPick.title = localize('pick an account', "Preferences Sync");
quickPick.ok = false;
quickPick.placeholder = localize('choose account placeholder', "Select an account");
quickPick.ignoreFocusOut = true;
quickPick.items = this.createQuickpickItems();
disposables.add(quickPick.onDidAccept(() => {
result = quickPick.selectedItems[0]?.account ? quickPick.selectedItems[0]?.account : quickPick.selectedItems[0]?.authenticationProvider;
quickPick.hide();
}));
disposables.add(quickPick.onDidHide(() => {
disposables.dispose();
c(result);
}));
quickPick.show();
});
}
private createQuickpickItems(): (AccountQuickPickItem | IQuickPickSeparator)[] {
const quickPickItems: (AccountQuickPickItem | IQuickPickSeparator)[] = [];
// Signed in Accounts
if (this.all.length) {
const authenticationProviders = [...this.authenticationProviders].sort(({ id }) => id === this.current?.authenticationProviderId ? -1 : 1);
quickPickItems.push({ type: 'separator', label: localize('signed in', "Signed in") });
for (const authenticationProvider of authenticationProviders) {
const accounts = (this._all.get(authenticationProvider.id) || []).sort(({ sessionId }) => sessionId === this.current?.sessionId ? -1 : 1);
const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
for (const account of accounts) {
quickPickItems.push({
label: `${account.accountName} (${providerName})`,
description: account.sessionId === this.current?.sessionId ? localize('last used', "Last Used with Sync") : undefined,
account,
authenticationProvider,
});
}
}
quickPickItems.push({ type: 'separator', label: localize('others', "Others") });
}
// Account proviers
for (const authenticationProvider of this.authenticationProviders) {
const providerName = this.authenticationService.getDisplayName(authenticationProvider.id);
quickPickItems.push({ label: localize('sign in using account', "Sign in with {0}", providerName), authenticationProvider });
}
return quickPickItems;
}
private async switch(sessionId: string, accountName: string, accountId: string): Promise<void> {
const currentAccount = this.current;
if (this.userDataSyncEnablementService.isEnabled() && (currentAccount && currentAccount.accountName !== accountName)) {
// accounts are switched while sync is enabled.
}
this.currentSessionId = sessionId;
this.telemetryService.publicLog2<UserAccountEvent, UserAccountClassification>('sync.userAccount', { id: accountId });
await this.update();
}
private onDidChangeSessions(e: AuthenticationSessionsChangeEvent): void {
if (this.currentSessionId && e.removed.includes(this.currentSessionId)) {
this.currentSessionId = undefined;
}
this.update();
}
private onDidChangeStorage(e: IWorkspaceStorageChangeEvent): void {
if (e.key === UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY && e.scope === StorageScope.GLOBAL
&& this.currentSessionId !== this.getStoredCachedSessionId() /* This checks if current window changed the value or not */) {
this._cachedCurrentSessionId = null;
this.update();
}
}
private _cachedCurrentSessionId: string | undefined | null = null;
private get currentSessionId(): string | undefined {
if (this._cachedCurrentSessionId === null) {
this._cachedCurrentSessionId = this.getStoredCachedSessionId();
}
return this._cachedCurrentSessionId;
}
private set currentSessionId(cachedSessionId: string | undefined) {
if (this._cachedCurrentSessionId !== cachedSessionId) {
this._cachedCurrentSessionId = cachedSessionId;
if (cachedSessionId === undefined) {
this.storageService.remove(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
} else {
this.storageService.store(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, cachedSessionId, StorageScope.GLOBAL);
}
}
}
private getStoredCachedSessionId(): string | undefined {
return this.storageService.get(UserDataSyncWorkbenchService.CACHED_SESSION_STORAGE_KEY, StorageScope.GLOBAL);
}
private get useWorkbenchSessionId(): boolean {
return !this.storageService.getBoolean(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, StorageScope.GLOBAL, false);
}
private set useWorkbenchSessionId(useWorkbenchSession: boolean) {
this.storageService.store(UserDataSyncWorkbenchService.DONOT_USE_WORKBENCH_SESSION_STORAGE_KEY, !useWorkbenchSession, StorageScope.GLOBAL);
}
}
registerSingleton(IUserDataSyncWorkbenchService, UserDataSyncWorkbenchService);

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IAuthenticationProvider, SyncStatus, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { Event } from 'vs/base/common/event';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { localize } from 'vs/nls';
export interface IUserDataSyncAccount {
readonly authenticationProviderId: string;
readonly accountName: string;
readonly accountId: string;
}
export const IUserDataSyncWorkbenchService = createDecorator<IUserDataSyncWorkbenchService>('IUserDataSyncWorkbenchService');
export interface IUserDataSyncWorkbenchService {
_serviceBrand: any;
readonly authenticationProviders: IAuthenticationProvider[];
readonly all: IUserDataSyncAccount[];
readonly current: IUserDataSyncAccount | undefined;
readonly accountStatus: AccountStatus;
readonly onDidChangeAccountStatus: Event<AccountStatus>;
turnOn(): Promise<void>;
turnoff(everyWhere: boolean): Promise<void>;
pickAccount(): Promise<void>;
}
export function getSyncAreaLabel(source: SyncResource): string {
switch (source) {
case SyncResource.Settings: return localize('settings', "Settings");
case SyncResource.Keybindings: return localize('keybindings', "Keyboard Shortcuts");
case SyncResource.Snippets: return localize('snippets', "User Snippets");
case SyncResource.Extensions: return localize('extensions', "Extensions");
case SyncResource.GlobalState: return localize('ui state label', "UI State");
}
}
export const enum AccountStatus {
Uninitialized = 'uninitialized',
Unavailable = 'unavailable',
Available = 'available',
}
// Contexts
export const CONTEXT_SYNC_STATE = new RawContextKey<string>('syncStatus', SyncStatus.Uninitialized);
export const CONTEXT_SYNC_ENABLEMENT = new RawContextKey<boolean>('syncEnabled', false);
export const CONTEXT_ACCOUNT_STATE = new RawContextKey<string>('userDataSyncAccountStatus', AccountStatus.Uninitialized);
export const CONTEXT_ENABLE_VIEWS = new RawContextKey<boolean>(`showUserDataSyncViews`, false);
// Commands
export const ENABLE_SYNC_VIEWS_COMMAND_ID = 'workbench.userDataSync.actions.enableViews';
export const CONFIGURE_SYNC_COMMAND_ID = 'workbench.userDataSync.actions.configure';
export const SHOW_SYNC_LOG_COMMAND_ID = 'workbench.userDataSync.actions.showLog';

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISharedProcessService } from 'vs/platform/ipc/electron-browser/sharedProcessService';
import { Disposable } from 'vs/base/common/lifecycle';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IUserDataSyncMachinesService, IUserDataSyncMachine } from 'vs/platform/userDataSync/common/userDataSyncMachines';
class UserDataSyncMachinesService extends Disposable implements IUserDataSyncMachinesService {
_serviceBrand: undefined;
private readonly channel: IChannel;
constructor(
@ISharedProcessService sharedProcessService: ISharedProcessService
) {
super();
this.channel = sharedProcessService.getChannel('userDataSyncMachines');
}
getMachines(): Promise<IUserDataSyncMachine[]> {
return this.channel.call<IUserDataSyncMachine[]>('getMachines');
}
addCurrentMachine(name: string): Promise<void> {
return this.channel.call('addCurrentMachine', [name]);
}
removeCurrentMachine(): Promise<void> {
return this.channel.call('removeCurrentMachine');
}
renameMachine(machineId: string, name: string): Promise<void> {
return this.channel.call('renameMachine', [machineId, name]);
}
disableMachine(machineId: string): Promise<void> {
return this.channel.call('disableMachine', [machineId]);
}
}
registerSingleton(IUserDataSyncMachinesService, UserDataSyncMachinesService);

View File

@@ -77,6 +77,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return this.channel.call('stop');
}
replace(uri: URI): Promise<void> {
return this.channel.call('replace', [uri]);
}
reset(): Promise<void> {
return this.channel.call('reset');
}
@@ -112,6 +116,10 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
return result.map(({ resource, comparableResource }) => ({ resource: URI.revive(resource), comparableResource: URI.revive(comparableResource) }));
}
async getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined> {
return this.channel.call<string | undefined>('getMachineId', [resource, syncResourceHandle]);
}
private async updateStatus(status: SyncStatus): Promise<void> {
this._status = status;
this._onDidChangeStatus.fire(status);

View File

@@ -5,7 +5,7 @@
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { StorageKeysSyncRegistryChannelClient } from 'vs/platform/userDataSync/common/userDataSyncIpc';
class StorageKeysSyncRegistryService extends StorageKeysSyncRegistryChannelClient implements IStorageKeysSyncRegistryService {
@@ -15,7 +15,6 @@ class StorageKeysSyncRegistryService extends StorageKeysSyncRegistryChannelClien
) {
super(mainProcessService.getChannel('storageKeysSyncRegistryService'));
}
}
registerSingleton(IStorageKeysSyncRegistryService, StorageKeysSyncRegistryService);

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { ViewContainerLocation, IViewDescriptorService, ViewContainer, IViewsRegistry, IViewContainersRegistry, IViewDescriptor, Extensions as ViewExtensions } from 'vs/workbench/common/views';
import { IContextKey, RawContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IContextKey, RawContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { Registry } from 'vs/platform/registry/common/platform';
@@ -18,10 +18,11 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { generateUuid } from 'vs/base/common/uuid';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ViewContainerModel } from 'vs/workbench/services/views/common/viewContainerModel';
import { registerAction2, Action2, MenuId } from 'vs/platform/actions/common/actions';
import { localize } from 'vs/nls';
interface ICachedViewContainerInfo {
containerId: string;
location?: ViewContainerLocation;
}
export class ViewDescriptorService extends Disposable implements IViewDescriptorService {
@@ -45,6 +46,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
private readonly activeViewContextKeys: Map<string, IContextKey<boolean>>;
private readonly movableViewContextKeys: Map<string, IContextKey<boolean>>;
private readonly defaultViewLocationContextKeys: Map<string, IContextKey<boolean>>;
private readonly defaultViewContainerLocationContextKeys: Map<string, IContextKey<boolean>>;
private readonly viewsRegistry: IViewsRegistry;
private readonly viewContainersRegistry: IViewContainersRegistry;
@@ -84,7 +86,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
}
}
readonly onDidChangeViewContainers: Event<{ added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }> }>;
private readonly _onDidChangeViewContainers = this._register(new Emitter<{ added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }> }>());
readonly onDidChangeViewContainers = this._onDidChangeViewContainers.event;
get viewContainers(): ReadonlyArray<ViewContainer> { return this.viewContainersRegistry.all; }
constructor(
@@ -98,34 +101,37 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
super();
storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_POSITIONS, version: 1 });
storageKeysSyncRegistryService.registerStorageKey({ key: ViewDescriptorService.CACHED_VIEW_CONTAINER_LOCATIONS, version: 1 });
this.viewContainerModels = new Map<ViewContainer, { viewContainerModel: ViewContainerModel, disposable: IDisposable; }>();
this.activeViewContextKeys = new Map<string, IContextKey<boolean>>();
this.movableViewContextKeys = new Map<string, IContextKey<boolean>>();
this.defaultViewLocationContextKeys = new Map<string, IContextKey<boolean>>();
this.defaultViewContainerLocationContextKeys = new Map<string, IContextKey<boolean>>();
this.viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry);
this.viewsRegistry = Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry);
this.cachedViewInfo = this.getCachedViewPositions();
this.cachedViewContainerInfo = this.getCachedViewContainerLocations();
this.cachedViewInfo = this.getCachedViewPositions();
// Register all containers that were registered before this ctor
this.viewContainers.forEach(viewContainer => this.onDidRegisterViewContainer(viewContainer));
this.onDidChangeViewContainers = Event.any<{ added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }> }>(
Event.map(this.viewContainersRegistry.onDidRegister, e => ({ added: [{ container: e.viewContainer, location: e.viewContainerLocation }], removed: [] })),
Event.map(this.viewContainersRegistry.onDidDeregister, e => ({ added: [], removed: [{ container: e.viewContainer, location: e.viewContainerLocation }] }))
);
// Try generating all generated containers that don't need extensions
this.tryGenerateContainers();
this._register(this.viewsRegistry.onViewsRegistered(views => this.onDidRegisterViews(views)));
this._register(this.viewsRegistry.onViewsDeregistered(({ views, viewContainer }) => this.onDidDeregisterViews(views, viewContainer)));
this._register(this.viewsRegistry.onDidChangeContainer(({ views, from, to }) => this.moveViews(views, from, to)));
this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer }) => this.onDidRegisterViewContainer(viewContainer)));
this._register(this.viewContainersRegistry.onDidDeregister(({ viewContainer }) => this.onDidDeregisterViewContainer(viewContainer)));
this._register(this.viewContainersRegistry.onDidRegister(({ viewContainer }) => {
this.onDidRegisterViewContainer(viewContainer);
this._onDidChangeViewContainers.fire({ added: [{ container: viewContainer, location: this.getViewContainerLocation(viewContainer) }], removed: [] });
}));
this._register(this.viewContainersRegistry.onDidDeregister(({ viewContainer }) => {
this.onDidDeregisterViewContainer(viewContainer);
this._onDidChangeViewContainers.fire({ removed: [{ container: viewContainer, location: this.getViewContainerLocation(viewContainer) }], added: [] });
}));
this._register(toDisposable(() => {
this.viewContainerModels.forEach(({ disposable }) => disposable.dispose());
this.viewContainerModels.clear();
@@ -144,18 +150,21 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
// The container has not been registered yet
if (!viewContainer || !this.viewContainerModels.has(viewContainer)) {
if (containerData.cachedContainerInfo && this.shouldGenerateContainer(containerData.cachedContainerInfo)) {
const containerInfo = containerData.cachedContainerInfo;
if (containerData.cachedContainerInfo && this.isGeneratedContainerId(containerData.cachedContainerInfo.containerId)) {
if (!this.viewContainersRegistry.get(containerId)) {
this.registerGeneratedViewContainer(containerInfo.location!, containerId);
this.registerGeneratedViewContainer(this.cachedViewContainerInfo.get(containerId)!, containerId);
}
}
// Registration of a generated container handles registration of its views
continue;
}
this.addViews(viewContainer, containerData.views);
// Filter out views that have already been added to the view container model
// This is needed when statically-registered views are moved to
// other statically registered containers as they will both try to add on startup
const viewsToAdd = containerData.views.filter(view => this.getViewContainerModel(viewContainer).allViewDescriptors.filter(vd => vd.id === view.id).length === 0);
this.addViews(viewContainer, viewsToAdd);
}
}
@@ -173,7 +182,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
}
}
private tryGenerateContainers(fallbackToDefault?: boolean): void {
private fallbackOrphanedViews(): void {
for (const [viewId, containerInfo] of this.cachedViewInfo.entries()) {
const containerId = containerInfo.containerId;
@@ -182,34 +191,18 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
continue;
}
// check if we should generate this container
if (this.shouldGenerateContainer(containerInfo)) {
this.registerGeneratedViewContainer(containerInfo.location!, containerId);
continue;
// check if view has been registered to default location
const viewContainer = this.viewsRegistry.getViewContainer(viewId);
const viewDescriptor = this.getViewDescriptorById(viewId);
if (viewContainer && viewDescriptor) {
this.addViews(viewContainer, [viewDescriptor]);
}
if (fallbackToDefault) {
// check if view has been registered to default location
const viewContainer = this.viewsRegistry.getViewContainer(viewId);
const viewDescriptor = this.getViewDescriptorById(viewId);
if (viewContainer && viewDescriptor) {
this.addViews(viewContainer, [viewDescriptor]);
const newLocation = this.getViewContainerLocation(viewContainer);
if (containerInfo.location && containerInfo.location !== newLocation) {
this._onDidChangeLocation.fire({ views: [viewDescriptor], from: containerInfo.location, to: newLocation });
}
}
}
}
if (fallbackToDefault) {
this.saveViewPositionsToCache();
}
}
private onDidRegisterExtensions(): void {
this.tryGenerateContainers(true);
// If an extension is uninstalled, this method will handle resetting views to default locations
this.fallbackOrphanedViews();
}
private onDidRegisterViews(views: { views: IViewDescriptor[], viewContainer: ViewContainer }[]): void {
@@ -226,8 +219,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
});
}
private shouldGenerateContainer(containerInfo: ICachedViewContainerInfo): boolean {
return containerInfo.containerId.startsWith(ViewDescriptorService.COMMON_CONTAINER_ID_PREFIX) && containerInfo.location !== undefined;
private isGeneratedContainerId(id: string): boolean {
return id.startsWith(ViewDescriptorService.COMMON_CONTAINER_ID_PREFIX);
}
private onDidDeregisterViews(views: IViewDescriptor[], viewContainer: ViewContainer): void {
@@ -257,17 +250,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
}
getViewLocationById(viewId: string): ViewContainerLocation | null {
const cachedInfo = this.cachedViewInfo.get(viewId);
if (cachedInfo && cachedInfo.location) {
return cachedInfo.location;
}
const container = cachedInfo?.containerId ?
this.viewContainersRegistry.get(cachedInfo.containerId) ?? null :
this.viewsRegistry.getViewContainer(viewId);
if (!container) {
const container = this.getViewContainerByViewId(viewId);
if (container === null) {
return null;
}
@@ -311,14 +295,17 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
return this.viewContainersRegistry.getDefaultViewContainer(location);
}
moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation): void {
moveViewContainerToLocation(viewContainer: ViewContainer, location: ViewContainerLocation, order?: number): void {
const from = this.getViewContainerLocation(viewContainer);
const to = location;
if (from !== to) {
if (this.getDefaultViewContainerLocation(viewContainer) === to) {
this.cachedViewContainerInfo.delete(viewContainer.id);
} else {
this.cachedViewContainerInfo.set(viewContainer.id, to);
this.cachedViewContainerInfo.set(viewContainer.id, to);
const defaultLocation = this.isGeneratedContainerId(viewContainer.id) ? true : this.getViewContainerLocation(viewContainer) === this.getDefaultViewContainerLocation(viewContainer);
this.getOrCreateDefaultViewContainerLocationContextKey(viewContainer).set(defaultLocation);
if (order !== undefined) {
viewContainer.order = order;
}
this._onDidChangeContainerLocation.fire({ viewContainer, from, to });
@@ -350,7 +337,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
private moveViews(views: IViewDescriptor[], from: ViewContainer, to: ViewContainer, skipCacheUpdate?: boolean): void {
this.removeViews(from, views);
this.addViews(to, views);
this.addViews(to, views, true);
const oldLocation = this.getViewContainerLocation(from);
const newLocation = this.getViewContainerLocation(to);
@@ -406,7 +393,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
private registerGeneratedViewContainer(location: ViewContainerLocation, existingId?: string): ViewContainer {
const id = existingId || this.generateContainerId(location);
return this.viewContainersRegistry.registerViewContainer({
const container = this.viewContainersRegistry.registerViewContainer({
id,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [id, { mergeViewWithContainerWhenSingleView: true, donotShowContainerTitleWhenMergedWithContainer: true }]),
name: 'Custom Views', // we don't want to see this, so no need to localize
@@ -414,6 +401,14 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
storageId: `${id}.state`,
hideIfEmpty: true
}, location);
const cachedInfo = this.cachedViewContainerInfo.get(container.id);
if (cachedInfo !== location) {
this.cachedViewContainerInfo.set(container.id, location);
this.saveViewContainerLocationsToCache();
}
return container;
}
private getCachedViewPositions(): Map<string, ICachedViewContainerInfo> {
@@ -423,6 +418,14 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
for (const [viewId, containerInfo] of result.entries()) {
if (!containerInfo) {
result.delete(viewId);
continue;
}
// Verify a view that is in a generated has cached container info
const generated = this.isGeneratedContainerId(containerInfo.containerId);
const missingCacheData = this.cachedViewContainerInfo.get(containerInfo.containerId) === undefined;
if (generated && missingCacheData) {
result.delete(viewId);
}
}
@@ -450,7 +453,10 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
const newViewContainerInfo = newCachedPositions.get(viewId)!;
// Verify if we need to create the destination container
if (!this.viewContainersRegistry.get(newViewContainerInfo.containerId)) {
this.registerGeneratedViewContainer(newViewContainerInfo.location!, newViewContainerInfo.containerId);
const location = this.cachedViewContainerInfo.get(newViewContainerInfo.containerId);
if (location !== undefined) {
this.registerGeneratedViewContainer(location, newViewContainerInfo.containerId);
}
}
// Try moving to the new container
@@ -540,10 +546,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
this.viewContainers.forEach(viewContainer => {
const viewContainerModel = this.getViewContainerModel(viewContainer);
viewContainerModel.allViewDescriptors.forEach(viewDescriptor => {
const containerLocation = this.getViewContainerLocation(viewContainer);
this.cachedViewInfo.set(viewDescriptor.id, {
containerId: viewContainer.id,
location: containerLocation
containerId: viewContainer.id
});
});
});
@@ -564,7 +568,7 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
private saveViewContainerLocationsToCache(): void {
for (const [containerId, location] of this.cachedViewContainerInfo) {
const container = this.getViewContainerById(containerId);
if (container && location === this.getDefaultViewContainerLocation(container)) {
if (container && location === this.getDefaultViewContainerLocation(container) && !this.isGeneratedContainerId(containerId)) {
this.cachedViewContainerInfo.delete(containerId);
}
}
@@ -597,6 +601,8 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
}
private onDidRegisterViewContainer(viewContainer: ViewContainer): void {
const defaultLocation = this.isGeneratedContainerId(viewContainer.id) ? true : this.getViewContainerLocation(viewContainer) === this.getDefaultViewContainerLocation(viewContainer);
this.getOrCreateDefaultViewContainerLocationContextKey(viewContainer).set(defaultLocation);
this.getOrRegisterViewContainerModel(viewContainer);
}
@@ -610,9 +616,17 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
this.onDidChangeActiveViews({ added: viewContainerModel.activeViewDescriptors, removed: [] });
viewContainerModel.onDidChangeActiveViewDescriptors(changed => this.onDidChangeActiveViews(changed), this, disposables);
disposables.add(this.registerResetViewContainerAction(viewContainer));
this.viewContainerModels.set(viewContainer, { viewContainerModel: viewContainerModel, disposable: disposables });
const viewsToRegister = this.getViewsByContainer(viewContainer);
// Register all views that were statically registered to this container
// Potentially, this is registering something that was handled by another container
// addViews() handles this by filtering views that are already registered
this.onDidRegisterViews([{ views: this.viewsRegistry.getViews(viewContainer), viewContainer }]);
// Add views that were registered prior to this view container
const viewsToRegister = this.getViewsByContainer(viewContainer).filter(view => this.getDefaultContainerById(view.id) !== viewContainer);
if (viewsToRegister.length) {
this.addViews(viewContainer, viewsToRegister);
viewsToRegister.forEach(viewDescriptor => this.getOrCreateMovableViewContextKey(viewDescriptor).set(!!viewDescriptor.canMoveView));
@@ -635,15 +649,41 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
removed.forEach(viewDescriptor => this.getOrCreateActiveViewContextKey(viewDescriptor).set(false));
}
private addViews(container: ViewContainer, views: IViewDescriptor[]): void {
private registerResetViewContainerAction(viewContainer: ViewContainer): IDisposable {
const that = this;
return registerAction2(class ResetViewLocationAction extends Action2 {
constructor() {
super({
id: `${viewContainer.id}.resetViewContainerLocation`,
title: {
original: 'Reset Location',
value: localize('resetViewLocation', "Reset Location")
},
menu: [{
id: MenuId.ViewContainerTitleContext,
when: ContextKeyExpr.or(
ContextKeyExpr.and(
ContextKeyExpr.equals('container', viewContainer.id),
ContextKeyExpr.equals(`${viewContainer.id}.defaultViewContainerLocation`, false)
)
)
}],
});
}
run(): void {
that.moveViewContainerToLocation(viewContainer, that.getDefaultViewContainerLocation(viewContainer));
}
});
}
private addViews(container: ViewContainer, views: IViewDescriptor[], expandViews?: boolean): void {
// Update in memory cache
const location = this.getViewContainerLocation(container);
views.forEach(view => {
this.cachedViewInfo.set(view.id, { containerId: container.id, location });
this.cachedViewInfo.set(view.id, { containerId: container.id });
this.getOrCreateDefaultViewLocationContextKey(view).set(this.getDefaultContainerById(view.id) === container);
});
this.getViewContainerModel(container).add(views);
this.getViewContainerModel(container).add(views.map(view => { return { viewDescriptor: view, collapsed: expandViews ? false : undefined }; }));
}
private removeViews(container: ViewContainer, views: IViewDescriptor[]): void {
@@ -683,6 +723,16 @@ export class ViewDescriptorService extends Disposable implements IViewDescriptor
}
return contextKey;
}
private getOrCreateDefaultViewContainerLocationContextKey(viewContainer: ViewContainer): IContextKey<boolean> {
const defaultViewContainerLocationContextKeyId = `${viewContainer.id}.defaultViewContainerLocation`;
let contextKey = this.defaultViewContainerLocationContextKeys.get(defaultViewContainerLocationContextKeyId);
if (!contextKey) {
contextKey = new RawContextKey(defaultViewContainerLocationContextKeyId, false).bindTo(this.contextKeyService);
this.defaultViewContainerLocationContextKeys.set(defaultViewContainerLocationContextKeyId, contextKey);
}
return contextKey;
}
}
registerSingleton(IViewDescriptorService, ViewDescriptorService);

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ViewContainer, IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions, IViewContainerModel, IAddedViewDescriptorRef, IViewDescriptorRef } from 'vs/workbench/common/views';
import { ViewContainer, IViewsRegistry, IViewDescriptor, Extensions as ViewExtensions, IViewContainerModel, IAddedViewDescriptorRef, IViewDescriptorRef, IAddedViewDescriptorState } from 'vs/workbench/common/views';
import { IContextKeyService, IReadableSet } from 'vs/platform/contextkey/common/contextkey';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { Registry } from 'vs/platform/registry/common/platform';
@@ -342,7 +342,7 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode
private updateContainerInfo(): void {
/* Use default container info if one of the visible view descriptors belongs to the current container by default */
const useDefaultContainerInfo = this.container.alwaysUseContainerInfo || this.visibleViewDescriptors.length === 0 || this.visibleViewDescriptors.some(v => Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).getViewContainer(v.id) === this.container);
const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.name || '';
const title = useDefaultContainerInfo ? this.container.name : this.visibleViewDescriptors[0]?.containerTitle || this.visibleViewDescriptors[0]?.name || '';
let titleChanged: boolean = false;
if (this._title !== title) {
this._title = title;
@@ -461,12 +461,13 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode
});
}
add(viewDescriptors: IViewDescriptor[]): void {
add(addedViewDescriptorStates: IAddedViewDescriptorState[]): void {
const addedItems: IViewDescriptorItem[] = [];
const addedActiveDescriptors: IViewDescriptor[] = [];
const addedVisibleItems: { index: number, viewDescriptor: IViewDescriptor, size?: number, collapsed: boolean; }[] = [];
for (const viewDescriptor of viewDescriptors) {
for (const addedViewDescriptorState of addedViewDescriptorStates) {
const viewDescriptor = addedViewDescriptorState.viewDescriptor;
if (viewDescriptor.when) {
for (const key of viewDescriptor.when.keys()) {
@@ -482,13 +483,13 @@ export class ViewContainerModel extends Disposable implements IViewContainerMode
} else {
state.visibleGlobal = isUndefinedOrNull(state.visibleGlobal) ? !viewDescriptor.hideByDefault : state.visibleGlobal;
}
state.collapsed = isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed;
state.collapsed = isUndefinedOrNull(addedViewDescriptorState.collapsed) ? (isUndefinedOrNull(state.collapsed) ? !!viewDescriptor.collapsed : state.collapsed) : addedViewDescriptorState.collapsed;
} else {
state = {
active: false,
visibleGlobal: !viewDescriptor.hideByDefault,
visibleWorkspace: !viewDescriptor.hideByDefault,
collapsed: !!viewDescriptor.collapsed,
collapsed: isUndefinedOrNull(addedViewDescriptorState.collapsed) ? !!viewDescriptor.collapsed : addedViewDescriptorState.collapsed,
};
}
this.viewDescriptorsState.set(viewDescriptor.id, state);

View File

@@ -14,7 +14,7 @@ import { ConfigurationScope, IConfigurationRegistry, Extensions as Configuration
import { Registry } from 'vs/platform/registry/common/platform';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { distinct } from 'vs/base/common/arrays';
import { isEqual, getComparisonKey } from 'vs/base/common/resources';
import { isEqual, getComparisonKey, isEqualAuthority } from 'vs/base/common/resources';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IFileService } from 'vs/platform/files/common/files';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -131,7 +131,7 @@ export abstract class AbstractWorkspaceEditingService implements IWorkspaceEditi
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
if (remoteAuthority) {
// https://github.com/microsoft/vscode/issues/94191
foldersToAdd = foldersToAdd.filter(f => f.uri.scheme !== Schemas.file && (f.uri.scheme !== Schemas.vscodeRemote || f.uri.authority === remoteAuthority));
foldersToAdd = foldersToAdd.filter(f => f.uri.scheme !== Schemas.file && (f.uri.scheme !== Schemas.vscodeRemote || isEqualAuthority(f.uri.authority, remoteAuthority)));
}
// If we are in no-workspace or single-folder workspace, adding folders has to

View File

@@ -27,7 +27,7 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { AbstractWorkspaceEditingService } from 'vs/workbench/services/workspaces/browser/abstractWorkspaceEditingService';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
import { isMacintosh } from 'vs/base/common/platform';
import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { BackupFileService } from 'vs/workbench/services/backup/common/backupFileService';

View File

@@ -4,11 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { IMainProcessService } from 'vs/platform/ipc/electron-browser/mainProcessService';
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { createChannelSender } from 'vs/base/parts/ipc/node/ipc';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { createChannelSender } from 'vs/base/parts/ipc/common/ipc';
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
export class NativeWorkspacesService {
@@ -16,9 +15,9 @@ export class NativeWorkspacesService {
constructor(
@IMainProcessService mainProcessService: IMainProcessService,
@IWorkbenchEnvironmentService environmentService: INativeWorkbenchEnvironmentService
@IElectronService electronService: IElectronService
) {
return createChannelSender<IWorkspacesService>(mainProcessService.getChannel('workspaces'), { context: environmentService.configuration.windowId });
return createChannelSender<IWorkspacesService>(mainProcessService.getChannel('workspaces'), { context: electronService.windowId });
}
}