Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c (#8525)

* Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c

* remove files we don't want

* fix hygiene

* update distro

* update distro

* fix hygiene

* fix strict nulls

* distro

* distro

* fix tests

* fix tests

* add another edit

* fix viewlet icon

* fix azure dialog

* fix some padding

* fix more padding issues
This commit is contained in:
Anthony Dresser
2019-12-04 19:28:22 -08:00
committed by GitHub
parent a8818ab0df
commit f5ce7fb2a5
1507 changed files with 42813 additions and 27370 deletions

View File

@@ -24,14 +24,16 @@ export class AccessibilityService extends AbstractAccessibilityService implement
_serviceBrand: undefined;
private _accessibilitySupport = AccessibilitySupport.Unknown;
private didSendTelemetry = false;
constructor(
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
@IContextKeyService readonly contextKeyService: IContextKeyService,
@IConfigurationService readonly configurationService: IConfigurationService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService configurationService: IConfigurationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super(contextKeyService, configurationService);
this.setAccessibilitySupport(environmentService.configuration.accessibilitySupport ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled);
}
alwaysUnderlineAccessKeys(): Promise<boolean> {
@@ -61,17 +63,13 @@ export class AccessibilityService extends AbstractAccessibilityService implement
this._accessibilitySupport = accessibilitySupport;
this._onDidChangeAccessibilitySupport.fire();
if (accessibilitySupport === AccessibilitySupport.Enabled) {
if (!this.didSendTelemetry && accessibilitySupport === AccessibilitySupport.Enabled) {
this._telemetryService.publicLog2<AccessibilityMetrics, AccessibilityMetricsClassification>('accessibility', { enabled: true });
this.didSendTelemetry = true;
}
}
getAccessibilitySupport(): AccessibilitySupport {
if (this._accessibilitySupport === AccessibilitySupport.Unknown) {
const config = this.environmentService.configuration;
this._accessibilitySupport = config?.accessibilitySupport ? AccessibilitySupport.Enabled : AccessibilitySupport.Disabled;
}
return this._accessibilitySupport;
}
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { ICredentialsService } from 'vs/platform/credentials/common/credentials';
import { Disposable } from 'vs/base/common/lifecycle';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { URI } from 'vs/base/common/uri';
const SERVICE_NAME = 'VS Code';
const ACCOUNT = 'MyAccount';
export class AuthTokenService extends Disposable implements IAuthTokenService {
_serviceBrand: undefined;
private _status: AuthTokenStatus = AuthTokenStatus.Initializing;
get status(): AuthTokenStatus { return this._status; }
private _onDidChangeStatus: Emitter<AuthTokenStatus> = this._register(new Emitter<AuthTokenStatus>());
readonly onDidChangeStatus: Event<AuthTokenStatus> = this._onDidChangeStatus.event;
readonly _onDidGetCallback: Emitter<URI> = this._register(new Emitter<URI>());
constructor(
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IQuickInputService private readonly quickInputService: IQuickInputService
) {
super();
this.getToken().then(token => {
if (token) {
this.setStatus(AuthTokenStatus.SignedIn);
} else {
this.setStatus(AuthTokenStatus.SignedOut);
}
});
}
async getToken(): Promise<string | undefined> {
const token = await this.credentialsService.getPassword(SERVICE_NAME, ACCOUNT);
if (token) {
return token;
}
return undefined; // {{SQL CARBON EDIT}} strict-null-check
}
async login(): Promise<void> {
const token = await this.quickInputService.input({ placeHolder: localize('enter token', "Please provide the auth bearer token"), ignoreFocusLost: true, });
if (token) {
await this.credentialsService.setPassword(SERVICE_NAME, ACCOUNT, token);
this.setStatus(AuthTokenStatus.SignedIn);
}
}
async refreshToken(): Promise<void> {
await this.logout();
}
async logout(): Promise<void> {
await this.credentialsService.deletePassword(SERVICE_NAME, ACCOUNT);
this.setStatus(AuthTokenStatus.SignedOut);
}
private setStatus(status: AuthTokenStatus): void {
if (this._status !== status) {
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
}

View File

@@ -9,6 +9,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IAuthTokenService, AuthTokenStatus } from 'vs/platform/auth/common/auth';
import { URI } from 'vs/base/common/uri';
export class AuthTokenService extends Disposable implements IAuthTokenService {
@@ -16,41 +17,43 @@ export class AuthTokenService extends Disposable implements IAuthTokenService {
private readonly channel: IChannel;
private _status: AuthTokenStatus = AuthTokenStatus.Disabled;
private _status: AuthTokenStatus = AuthTokenStatus.Initializing;
get status(): AuthTokenStatus { return this._status; }
private _onDidChangeStatus: Emitter<AuthTokenStatus> = this._register(new Emitter<AuthTokenStatus>());
readonly onDidChangeStatus: Event<AuthTokenStatus> = this._onDidChangeStatus.event;
readonly _onDidGetCallback: Emitter<URI> = this._register(new Emitter<URI>());
constructor(
@ISharedProcessService sharedProcessService: ISharedProcessService
@ISharedProcessService sharedProcessService: ISharedProcessService,
) {
super();
this.channel = sharedProcessService.getChannel('authToken');
this.channel.call<AuthTokenStatus>('_getInitialStatus').then(status => {
this.updateStatus(status);
this._register(this.channel.listen<AuthTokenStatus>('onDidChangeStatus')(status => this.updateStatus(status)));
});
this._register(this.channel.listen<AuthTokenStatus>('onDidChangeStatus')(status => this.updateStatus(status)));
this.channel.call<AuthTokenStatus>('_getInitialStatus').then(status => this.updateStatus(status));
}
getToken(): Promise<string> {
return this.channel.call('getToken');
}
updateToken(token: string): Promise<void> {
return this.channel.call('updateToken', [token]);
login(): Promise<void> {
return this.channel.call('login');
}
refreshToken(): Promise<void> {
return this.channel.call('getToken');
}
deleteToken(): Promise<void> {
return this.channel.call('deleteToken');
logout(): Promise<void> {
return this.channel.call('logout');
}
private async updateStatus(status: AuthTokenStatus): Promise<void> {
this._status = status;
this._onDidChangeStatus.fire(status);
if (status !== AuthTokenStatus.Initializing) {
this._status = status;
this._onDidChangeStatus.fire(status);
}
}
}

View File

@@ -6,8 +6,6 @@
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITextBufferFactory, ITextSnapshot } from 'vs/editor/common/model';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, relativePath } from 'vs/base/common/resources';
export const IBackupFileService = createDecorator<IBackupFileService>('backupFileService');
@@ -88,7 +86,3 @@ export interface IBackupFileService {
*/
discardAllWorkspaceBackups(): Promise<void>;
}
export function toBackupWorkspaceResource(backupWorkspacePath: string, environmentService: IEnvironmentService): URI {
return joinPath(environmentService.userRoamingDataHome, relativePath(URI.file(environmentService.userDataPath), URI.file(backupWorkspacePath))!);
}

View File

@@ -429,13 +429,13 @@ export class InMemoryBackupFileService implements IBackupFileService {
return Promise.resolve();
}
resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
async resolveBackupContent<T extends object>(backupResource: URI): Promise<IResolvedBackup<T>> {
const snapshot = this.backups.get(backupResource.toString());
if (snapshot) {
return Promise.resolve({ value: createTextBufferFactoryFromSnapshot(snapshot) });
return { value: createTextBufferFactoryFromSnapshot(snapshot) };
}
return Promise.reject('Unexpected backup resource to resolve');
throw new Error('Unexpected backup resource to resolve');
}
getWorkspaceFileBackups(): Promise<URI[]> {

View File

@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* 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 { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { joinPath, relativePath } from 'vs/base/common/resources';
export function toBackupWorkspaceResource(backupWorkspacePath: string, environmentService: IEnvironmentService): URI {
return joinPath(environmentService.userRoamingDataHome, relativePath(URI.file(environmentService.userDataPath), URI.file(backupWorkspacePath))!);
}

View File

@@ -20,7 +20,7 @@ import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { FileService } from 'vs/platform/files/common/fileService';
import { NullLogService } from 'vs/platform/log/common/log';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { snapshotToString } from 'vs/workbench/services/textfile/common/textfiles';
import { IFileService } from 'vs/platform/files/common/files';
@@ -46,7 +46,7 @@ const fooBackupPath = path.join(workspaceBackupPath, 'file', hashPath(fooFile));
const barBackupPath = path.join(workspaceBackupPath, 'file', hashPath(barFile));
const untitledBackupPath = path.join(workspaceBackupPath, 'untitled', hashPath(untitledFile));
class TestBackupEnvironmentService extends WorkbenchEnvironmentService {
class TestBackupEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(backupPath: string) {
super({ ...parseArgs(process.argv, OPTIONS), ...{ backupPath, 'user-data-dir': userdataDir } } as IWindowConfiguration, process.execPath, 0);

View File

@@ -226,17 +226,18 @@ class BulkEditModel implements IDisposable {
}
}
export type Edit = ResourceFileEdit | ResourceTextEdit;
type Edit = ResourceFileEdit | ResourceTextEdit;
export class BulkEdit {
class BulkEdit {
private _edits: Edit[] = [];
private _editor: ICodeEditor | undefined;
private _progress: IProgress<IProgressStep>;
private readonly _edits: Edit[] = [];
private readonly _editor: ICodeEditor | undefined;
private readonly _progress: IProgress<IProgressStep>;
constructor(
editor: ICodeEditor | undefined,
progress: IProgress<IProgressStep> | undefined,
edits: Edit[],
@ILogService private readonly _logService: ILogService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@@ -246,14 +247,7 @@ export class BulkEdit {
) {
this._editor = editor;
this._progress = progress || emptyProgress;
}
add(edits: Edit[] | Edit): void {
if (Array.isArray(edits)) {
this._edits.push(...edits);
} else {
this._edits.push(edits);
}
this._edits = edits;
}
ariaMessage(): string {
@@ -419,8 +413,11 @@ export class BulkEditService implements IBulkEditService {
// If the code editor is readonly still allow bulk edits to be applied #68549
codeEditor = undefined;
}
const bulkEdit = new BulkEdit(codeEditor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService);
bulkEdit.add(edits);
const bulkEdit = new BulkEdit(
codeEditor, options.progress, edits,
this._logService, this._textModelService, this._fileService, this._textFileService, this._labelService, this._configurationService
);
return bulkEdit.perform().then(() => {
return { ariaSummary: bulkEdit.ariaMessage() };

View File

@@ -18,7 +18,28 @@ export class BrowserClipboardService implements IClipboardService {
return; // TODO@sbatten
}
return navigator.clipboard.writeText(text);
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
} else {
const activeElement = <HTMLElement>document.activeElement;
const newTextarea = document.createElement('textarea');
newTextarea.className = 'clipboard-copy';
newTextarea.style.visibility = 'false';
newTextarea.style.height = '1px';
newTextarea.style.width = '1px';
newTextarea.setAttribute('aria-hidden', 'true');
newTextarea.style.position = 'absolute';
newTextarea.style.top = '-1000';
newTextarea.style.left = '-1000';
document.body.appendChild(newTextarea);
newTextarea.value = text;
newTextarea.focus();
newTextarea.select();
document.execCommand('copy');
activeElement.focus();
document.body.removeChild(newTextarea);
}
return;
}
async readText(type?: string): Promise<string> {

View File

@@ -33,7 +33,7 @@ export class WorkspaceService extends Disposable implements IConfigurationServic
public _serviceBrand: undefined;
private workspace: Workspace;
private workspace!: Workspace;
private completeWorkspaceBarrier: Barrier;
private readonly configurationCache: IConfigurationCache;
private _configuration: Configuration;

View File

@@ -158,7 +158,7 @@ export class ConfigurationEditingService {
writeConfiguration(target: EditableConfigurationTarget, value: IConfigurationValue, options: IConfigurationEditingOptions = {}): Promise<void> {
const operation = this.getConfigurationEditOperation(target, value, options.scopes || {});
return Promise.resolve(this.queue.queue(() => this.doWriteConfiguration(operation, options) // queue up writes to prevent race conditions
.then(() => null,
.then(() => { },
error => {
if (!options.donotNotifyError) {
this.onError(error, operation, options.scopes);
@@ -409,7 +409,7 @@ export class ConfigurationEditingService {
return false;
}
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors);
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
return parseErrors.length > 0;
}
@@ -436,7 +436,7 @@ export class ConfigurationEditingService {
}
if (target === EditableConfigurationTarget.WORKSPACE) {
if (!operation.workspaceStandAloneConfigurationKey) {
if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
if (configurationProperties[operation.key].scope === ConfigurationScope.APPLICATION) {
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_WORKSPACE_CONFIGURATION_APPLICATION, target, operation);
@@ -452,7 +452,7 @@ export class ConfigurationEditingService {
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_TARGET, target, operation);
}
if (!operation.workspaceStandAloneConfigurationKey) {
if (!operation.workspaceStandAloneConfigurationKey && !OVERRIDE_PROPERTY_PATTERN.test(operation.key)) {
const configurationProperties = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
if (configurationProperties[operation.key].scope !== ConfigurationScope.RESOURCE) {
return this.reject(ConfigurationEditingErrorCode.ERROR_INVALID_FOLDER_CONFIGURATION, target, operation);

View File

@@ -98,7 +98,7 @@ export class JSONEditingService implements IJSONEditingService {
private hasParseErrors(model: ITextModel): boolean {
const parseErrors: json.ParseError[] = [];
json.parse(model.getValue(), parseErrors);
json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
return parseErrors.length > 0;
}

View File

@@ -18,7 +18,7 @@ import * as uuid from 'vs/base/common/uuid';
import { IConfigurationRegistry, Extensions as ConfigurationExtensions } from 'vs/platform/configuration/common/configurationRegistry';
import { WorkspaceService } from 'vs/workbench/services/configuration/browser/configurationService';
import { ConfigurationEditingService, ConfigurationEditingError, ConfigurationEditingErrorCode, EditableConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditingService';
import { WORKSPACE_STANDALONE_CONFIGURATIONS } from 'vs/workbench/services/configuration/common/configuration';
import { WORKSPACE_STANDALONE_CONFIGURATIONS, FOLDER_SETTINGS_PATH } from 'vs/workbench/services/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
@@ -39,11 +39,11 @@ import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemPro
import { IFileService } from 'vs/platform/files/common/files';
import { ConfigurationCache } from 'vs/workbench/services/configuration/node/configurationCache';
import { KeybindingsEditingService, IKeybindingEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
class TestEnvironmentService extends WorkbenchEnvironmentService {
class TestEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);
@@ -236,6 +236,41 @@ suite('ConfigurationEditingService', () => {
});
});
test('write overridable settings to user settings', () => {
const key = '[language]';
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
return testObject.writeConfiguration(EditableConfigurationTarget.USER_LOCAL, { key, value })
.then(() => {
const contents = fs.readFileSync(globalSettingsFile).toString('utf8');
const parsed = json.parse(contents);
assert.deepEqual(parsed[key], value);
});
});
test('write overridable settings to workspace settings', () => {
const key = '[language]';
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key, value })
.then(() => {
const target = path.join(workspaceDir, FOLDER_SETTINGS_PATH);
const contents = fs.readFileSync(target).toString('utf8');
const parsed = json.parse(contents);
assert.deepEqual(parsed[key], value);
});
});
test('write overridable settings to workspace folder settings', () => {
const key = '[language]';
const value = { 'configurationEditing.service.testSetting': 'overridden value' };
const folderSettingsFile = path.join(workspaceDir, FOLDER_SETTINGS_PATH);
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE_FOLDER, { key, value }, { scopes: { resource: URI.file(folderSettingsFile) } })
.then(() => {
const contents = fs.readFileSync(folderSettingsFile).toString('utf8');
const parsed = json.parse(contents);
assert.deepEqual(parsed[key], value);
});
});
test('write workspace standalone setting - empty file', () => {
return testObject.writeConfiguration(EditableConfigurationTarget.WORKSPACE, { key: 'tasks.service.testSetting', value: 'value' })
.then(() => {

View File

@@ -45,10 +45,10 @@ import { IConfigurationCache } from 'vs/workbench/services/configuration/common/
import { SignService } from 'vs/platform/sign/browser/signService';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { IKeybindingEditingService, KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
class TestEnvironmentService extends WorkbenchEnvironmentService {
class TestEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);

View File

@@ -32,17 +32,24 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
_serviceBrand: undefined;
private _context: IVariableResolveContext;
private _envVariables?: IProcessEnvironment;
protected _contributedVariables: Map<string, () => Promise<string | undefined>> = new Map();
constructor(
private _context: IVariableResolveContext,
private _envVariables: IProcessEnvironment
) {
if (isWindows && _envVariables) {
this._envVariables = Object.create(null);
Object.keys(_envVariables).forEach(key => {
this._envVariables[key.toLowerCase()] = _envVariables[key];
});
constructor(_context: IVariableResolveContext, _envVariables?: IProcessEnvironment) {
this._context = _context;
if (_envVariables) {
if (isWindows) {
// windows env variables are case insensitive
const ev: IProcessEnvironment = Object.create(null);
this._envVariables = ev;
Object.keys(_envVariables).forEach(key => {
ev[key.toLowerCase()] = _envVariables[key];
});
} else {
this._envVariables = _envVariables;
}
}
}
@@ -180,14 +187,13 @@ export class AbstractVariableResolverService implements IConfigurationResolverSe
case 'env':
if (argument) {
if (isWindows) {
argument = argument.toLowerCase();
if (this._envVariables) {
const env = this._envVariables[isWindows ? argument.toLowerCase() : argument];
if (types.isString(env)) {
return env;
}
}
const env = this._envVariables[argument];
if (types.isString(env)) {
return env;
}
// For `env` we should do the same as a normal shell does - evaluates missing envs to an empty string #46436
// For `env` we should do the same as a normal shell does - evaluates undefined envs to an empty string #46436
return '';
}
throw new Error(localize('missingEnvVarName', "'{0}' can not be resolved because no environment variable name is given.", match));

View File

@@ -19,7 +19,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import * as Types from 'vs/base/common/types';
import { EditorType } from 'vs/editor/common/editorCommon';
import { Selection } from 'vs/editor/common/core/selection';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -642,7 +642,7 @@ class MockInputsConfigurationService extends TestConfigurationService {
}
}
class MockWorkbenchEnvironmentService extends WorkbenchEnvironmentService {
class MockWorkbenchEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(env: platform.IProcessEnvironment) {
super({ userEnv: env } as IWindowConfiguration, process.execPath, 0);

View File

@@ -91,18 +91,25 @@ class NativeContextMenuService extends Disposable implements IContextMenuService
const anchor = delegate.getAnchor();
let x: number, y: number;
let zoom = webFrame.getZoomFactor();
if (dom.isHTMLElement(anchor)) {
let elementPosition = dom.getDomNodePagePosition(anchor);
x = elementPosition.left;
y = elementPosition.top + elementPosition.height;
// Shift macOS menus by a few pixels below elements
// to account for extra padding on top of native menu
// https://github.com/microsoft/vscode/issues/84231
if (isMacintosh) {
y += 4 / zoom;
}
} else {
const pos: { x: number; y: number; } = anchor;
x = pos.x + 1; /* prevent first item from being selected automatically under mouse */
y = pos.y;
}
let zoom = webFrame.getZoomFactor();
x *= zoom;
y *= zoom;

View File

@@ -20,7 +20,7 @@ export interface IDecorationData {
readonly bubble?: boolean;
}
export interface IDecoration {
export interface IDecoration extends IDisposable {
readonly tooltip: string;
readonly labelClassName: string;
readonly badgeClassName: string;

View File

@@ -12,14 +12,13 @@ import { isThenable } from 'vs/base/common/async';
import { LinkedList } from 'vs/base/common/linkedList';
import { createStyleSheet, createCSSRule, removeCSSRulesContainingSelector } from 'vs/base/browser/dom';
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { Iterator } from 'vs/base/common/iterator';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { localize } from 'vs/nls';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { hash } from 'vs/base/common/hash';
class DecorationRule {
@@ -32,18 +31,29 @@ class DecorationRule {
}
}
private static readonly _classNames = new IdGenerator('monaco-decorations-style-');
private static readonly _classNamesPrefix = 'monaco-decoration';
readonly data: IDecorationData | IDecorationData[];
readonly itemColorClassName: string;
readonly itemBadgeClassName: string;
readonly bubbleBadgeClassName: string;
constructor(data: IDecorationData | IDecorationData[]) {
private _refCounter: number = 0;
constructor(data: IDecorationData | IDecorationData[], key: string) {
this.data = data;
this.itemColorClassName = DecorationRule._classNames.nextId();
this.itemBadgeClassName = DecorationRule._classNames.nextId();
this.bubbleBadgeClassName = DecorationRule._classNames.nextId();
const suffix = hash(key).toString(36);
this.itemColorClassName = `${DecorationRule._classNamesPrefix}-itemColor-${suffix}`;
this.itemBadgeClassName = `${DecorationRule._classNamesPrefix}-itemBadge-${suffix}`;
this.bubbleBadgeClassName = `${DecorationRule._classNamesPrefix}-bubbleBadge-${suffix}`;
}
acquire(): void {
this._refCounter += 1;
}
release(): boolean {
return --this._refCounter === 0;
}
appendCSSRules(element: HTMLStyleElement, theme: ITheme): void {
@@ -89,12 +99,6 @@ class DecorationRule {
removeCSSRulesContainingSelector(this.itemBadgeClassName, element);
removeCSSRulesContainingSelector(this.bubbleBadgeClassName, element);
}
isUnused(): boolean {
return !document.querySelector(`.${this.itemColorClassName}`)
&& !document.querySelector(`.${this.itemBadgeClassName}`)
&& !document.querySelector(`.${this.bubbleBadgeClassName}`);
}
}
class DecorationStyles {
@@ -124,11 +128,13 @@ class DecorationStyles {
if (!rule) {
// new css rule
rule = new DecorationRule(data);
rule = new DecorationRule(data, key);
this._decorationRules.set(key, rule);
rule.appendCSSRules(this._styleElement, this._themeService.getTheme());
}
rule.acquire();
let labelClassName = rule.itemColorClassName;
let badgeClassName = rule.itemBadgeClassName;
let tooltip = data.filter(d => !isFalsyOrWhitespace(d.tooltip)).map(d => d.tooltip).join(' • ');
@@ -142,7 +148,14 @@ class DecorationStyles {
return {
labelClassName,
badgeClassName,
tooltip
tooltip,
dispose: () => {
if (rule && rule.release()) {
this._decorationRules.delete(key);
rule.removeCSSRules(this._styleElement);
rule = undefined;
}
}
};
}
@@ -152,34 +165,6 @@ class DecorationStyles {
rule.appendCSSRules(this._styleElement, this._themeService.getTheme());
});
}
cleanUp(iter: Iterator<DecorationProviderWrapper>): void {
// remove every rule for which no more
// decoration (data) is kept. this isn't cheap
let usedDecorations = new Set<string>();
for (let e = iter.next(); !e.done; e = iter.next()) {
e.value.data.forEach((value, key) => {
if (value && !(value instanceof DecorationDataRequest)) {
usedDecorations.add(DecorationRule.keyOf(value));
}
});
}
this._decorationRules.forEach((value, index) => {
const { data } = value;
if (value.isUnused()) {
let remove: boolean = false;
if (Array.isArray(data)) {
remove = data.every(data => !usedDecorations.has(DecorationRule.keyOf(data)));
} else if (!usedDecorations.has(DecorationRule.keyOf(data))) {
remove = true;
}
if (remove) {
value.removeCSSRules(this._styleElement);
this._decorationRules.delete(index);
}
}
});
}
}
class FileDecorationChangeEvent implements IResourceDecorationChangeEvent {
@@ -345,15 +330,6 @@ export class DecorationsService implements IDecorationsService {
@ILogService private readonly _logService: ILogService,
) {
this._decorationStyles = new DecorationStyles(themeService);
// every so many events we check if there are
// css styles that we don't need anymore
let count = 0;
this.onDidChangeDecorations(() => {
if (++count % 17 === 0) {
this._decorationStyles.cleanUp(this._data.iterator());
}
});
}
dispose(): void {

View File

@@ -5,7 +5,7 @@
import * as nls from 'vs/nls';
import { IWindowOpenable } from 'vs/platform/windows/common/windows';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter } from 'vs/platform/dialogs/common/dialogs';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, FileFilter, IFileDialogService, IDialogService, ConfirmResult, getConfirmMessage } from 'vs/platform/dialogs/common/dialogs';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -14,14 +14,15 @@ import { Schemas } from 'vs/base/common/network';
import * as resources from 'vs/base/common/resources';
import { IInstantiationService, } from 'vs/platform/instantiation/common/instantiation';
import { SimpleFileDialog } from 'vs/workbench/services/dialogs/browser/simpleFileDialog';
import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { WORKSPACE_EXTENSION, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService } from 'vs/platform/files/common/files';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import Severity from 'vs/base/common/severity';
export abstract class AbstractFileDialogService {
export abstract class AbstractFileDialogService implements IFileDialogService {
_serviceBrand: undefined;
@@ -34,6 +35,7 @@ export abstract class AbstractFileDialogService {
@IConfigurationService protected readonly configurationService: IConfigurationService,
@IFileService protected readonly fileService: IFileService,
@IOpenerService protected readonly openerService: IOpenerService,
@IDialogService private readonly dialogService: IDialogService
) { }
defaultFilePath(schemeFilter = this.getSchemeFilterForWindow()): URI | undefined {
@@ -78,6 +80,40 @@ export abstract class AbstractFileDialogService {
return this.defaultFilePath(schemeFilter);
}
async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
}
if (fileNamesOrResources.length === 0) {
return ConfirmResult.DONT_SAVE;
}
let message: string;
if (fileNamesOrResources.length === 1) {
message = nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", typeof fileNamesOrResources[0] === 'string' ? fileNamesOrResources[0] : resources.basename(fileNamesOrResources[0]));
} else {
message = getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", fileNamesOrResources.length), fileNamesOrResources);
}
const buttons: string[] = [
fileNamesOrResources.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
nls.localize('cancel', "Cancel")
];
const { choice } = await this.dialogService.show(Severity.Warning, message, buttons, {
cancelId: 2,
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
});
switch (choice) {
case 0: return ConfirmResult.SAVE;
case 1: return ConfirmResult.DONT_SAVE;
default: return ConfirmResult.CANCEL;
}
}
protected abstract addFileSchemaIfNeeded(schema: string): string[];
protected async pickFileFolderAndOpenSimplified(schema: string, options: IPickAndOpenOptions, preferNewWindow: boolean): Promise<any> {
@@ -179,8 +215,12 @@ export abstract class AbstractFileDialogService {
protected getFileSystemSchema(options: { availableFileSystems?: readonly string[], defaultUri?: URI }): string {
return options.availableFileSystems && options.availableFileSystems[0] || this.getSchemeFilterForWindow();
}
}
function isUntitledWorkspace(path: URI, environmentService: IWorkbenchEnvironmentService): boolean {
return resources.isEqualOrParent(path, environmentService.untitledWorkspacesHome);
abstract pickFileFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickFileAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickFolderAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickWorkspaceAndOpen(options: IPickAndOpenOptions): Promise<void>;
abstract pickFileToSave(options: ISaveDialogOptions): Promise<URI | undefined>;
abstract showSaveDialog(options: ISaveDialogOptions): Promise<URI | undefined>;
abstract showOpenDialog(options: IOpenDialogOptions): Promise<URI[] | undefined>;
}

View File

@@ -32,10 +32,9 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { ICommandHandler } from 'vs/platform/commands/common/commands';
import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { toResource } from 'vs/workbench/common/editor';
import { normalizeDriveLetter } from 'vs/base/common/labels';
import { SaveReason } from 'vs/workbench/common/editor';
export namespace OpenLocalFileCommand {
export const ID = 'workbench.action.files.openLocalFile';
@@ -53,13 +52,12 @@ export namespace SaveLocalFileCommand {
export const LABEL = nls.localize('saveLocalFile', "Save Local File...");
export function handler(): ICommandHandler {
return accessor => {
const textFileService = accessor.get(ITextFileService);
const editorService = accessor.get(IEditorService);
let resource: URI | undefined = toResource(editorService.activeEditor);
const options: ISaveOptions = { force: true, availableFileSystems: [Schemas.file] };
if (resource) {
return textFileService.saveAs(resource, undefined, options);
const activeControl = editorService.activeControl;
if (activeControl) {
return editorService.save({ groupId: activeControl.group.id, editor: activeControl.input }, { saveAs: true, availableFileSystems: [Schemas.file], reason: SaveReason.EXPLICIT });
}
return Promise.resolve(undefined);
};
}
@@ -262,6 +260,7 @@ export class SimpleFileDialog {
this.filePickBox = this.quickInputService.createQuickPick<FileQuickPickItem>();
this.busy = true;
this.filePickBox.matchOnLabel = false;
this.filePickBox.sortByLabel = false;
this.filePickBox.autoFocusOnList = false;
this.filePickBox.ignoreFocusOut = true;
this.filePickBox.ok = true;
@@ -373,28 +372,7 @@ export class SimpleFileDialog {
});
this.filePickBox.onDidChangeValue(async value => {
try {
// onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything
if (this.isValueChangeFromUser()) {
// If the user has just entered more bad path, don't change anything
if (!equalsIgnoreCase(value, this.constructFullUserPath()) && !this.isBadSubpath(value)) {
this.filePickBox.validationMessage = undefined;
const filePickBoxUri = this.filePickBoxValue();
let updated: UpdateResult = UpdateResult.NotUpdated;
if (!resources.isEqual(this.currentFolder, filePickBoxUri, true)) {
updated = await this.tryUpdateItems(value, filePickBoxUri);
}
if (updated === UpdateResult.NotUpdated) {
this.setActiveItems(value);
}
} else {
this.filePickBox.activeItems = [];
this.userEnteredPathSegment = '';
}
}
} catch {
// Since any text can be entered in the input box, there is potential for error causing input. If this happens, do nothing.
}
return this.handleValueChange(value);
});
this.filePickBox.onDidHide(() => {
this.hidden = true;
@@ -415,6 +393,31 @@ export class SimpleFileDialog {
});
}
private async handleValueChange(value: string) {
try {
// onDidChangeValue can also be triggered by the auto complete, so if it looks like the auto complete, don't do anything
if (this.isValueChangeFromUser()) {
// If the user has just entered more bad path, don't change anything
if (!equalsIgnoreCase(value, this.constructFullUserPath()) && !this.isBadSubpath(value)) {
this.filePickBox.validationMessage = undefined;
const filePickBoxUri = this.filePickBoxValue();
let updated: UpdateResult = UpdateResult.NotUpdated;
if (!resources.isEqual(this.currentFolder, filePickBoxUri, true)) {
updated = await this.tryUpdateItems(value, filePickBoxUri);
}
if (updated === UpdateResult.NotUpdated) {
this.setActiveItems(value);
}
} else {
this.filePickBox.activeItems = [];
this.userEnteredPathSegment = '';
}
}
} catch {
// Since any text can be entered in the input box, there is potential for error causing input. If this happens, do nothing.
}
}
private isBadSubpath(value: string) {
return this.badPath && (value.length > this.badPath.length) && equalsIgnoreCase(value.substring(0, this.badPath.length), this.badPath);
}
@@ -514,6 +517,16 @@ export class SimpleFileDialog {
return undefined;
}
private root(value: URI) {
let lastDir = value;
let dir = resources.dirname(value);
while (!resources.isEqual(lastDir, dir)) {
lastDir = dir;
dir = resources.dirname(dir);
}
return dir;
}
private async tryUpdateItems(value: string, valueUri: URI): Promise<UpdateResult> {
if ((value.length > 0) && ((value[value.length - 1] === '~') || (value[0] === '~'))) {
let newDir = this.userHome;
@@ -522,6 +535,11 @@ export class SimpleFileDialog {
}
await this.updateItems(newDir, true);
return UpdateResult.Updated;
} else if (value === '\\') {
valueUri = this.root(this.currentFolder);
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)))) {
let stat: IFileStat | undefined;
try {
@@ -535,7 +553,7 @@ export class SimpleFileDialog {
} else if (this.endsWithSlash(value)) {
// The input box contains a path that doesn't exist on the system.
this.filePickBox.validationMessage = nls.localize('remoteFileDialog.badPath', 'The path does not exist.');
// Save this bad path. It can take too long to to a stat on every user entered character, but once a user enters a bad path they are likely
// Save this bad path. It can take too long to a stat on every user entered character, but once a user enters a bad path they are likely
// to keep typing more bad path. We can compare against this bad path and see if the user entered path starts with it.
this.badPath = value;
return UpdateResult.InvalidPath;
@@ -602,7 +620,7 @@ export class SimpleFileDialog {
this.activeItem = quickPickItem;
if (force) {
// clear any selected text
this.insertText(this.userEnteredPathSegment, '');
document.execCommand('insertText', false, '');
}
return false;
} else if (!force && (itemBasename.length >= startingBasename.length) && equalsIgnoreCase(itemBasename.substr(0, startingBasename.length), startingBasename)) {
@@ -631,14 +649,19 @@ export class SimpleFileDialog {
private insertText(wholeValue: string, insertText: string) {
if (this.filePickBox.inputHasFocus()) {
document.execCommand('insertText', false, insertText);
if (this.filePickBox.value !== wholeValue) {
this.filePickBox.value = wholeValue;
this.handleValueChange(wholeValue);
}
} else {
this.filePickBox.value = wholeValue;
this.handleValueChange(wholeValue);
}
}
private addPostfix(uri: URI): URI {
let result = uri;
if (this.requiresTrailing && this.options.filters && this.options.filters.length > 0) {
if (this.requiresTrailing && this.options.filters && this.options.filters.length > 0 && !resources.hasTrailingPathSeparator(uri)) {
// Make sure that the suffix is added. If the user deleted it, we automatically add it here
let hasExt: boolean = false;
const currentExt = resources.extname(uri).substr(1);

View File

@@ -6,7 +6,7 @@
import { SaveDialogOptions, OpenDialogOptions } from 'electron';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/node/dialogs';
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IPickAndOpenOptions, ISaveDialogOptions, IOpenDialogOptions, IFileDialogService, IDialogService, ConfirmResult } 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';
@@ -33,8 +33,11 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
@IConfigurationService configurationService: IConfigurationService,
@IFileService fileService: IFileService,
@IOpenerService openerService: IOpenerService,
@IElectronService private readonly electronService: IElectronService
) { super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService); }
@IElectronService private readonly electronService: IElectronService,
@IDialogService dialogService: IDialogService
) {
super(hostService, contextService, historyService, environmentService, instantiationService, configurationService, fileService, openerService, dialogService);
}
private toNativeOpenDialogOptions(options: IPickAndOpenOptions): INativeOpenDialogOptions {
return {
@@ -180,6 +183,16 @@ export class FileDialogService extends AbstractFileDialogService implements IFil
// Don't allow untitled schema through.
return schema === Schemas.untitled ? [Schemas.file] : (schema !== Schemas.file ? [schema, Schemas.file] : [schema]);
}
async showSaveConfirm(fileNamesOrResources: (string | URI)[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
if (!this.environmentService.args['extension-development-confirm-save']) {
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
}
}
return super.showSaveConfirm(fileNamesOrResources);
}
}
registerSingleton(IFileDialogService, FileDialogService, true);

View File

@@ -5,12 +5,11 @@
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IResourceInput, ITextEditorOptions, IEditorOptions, EditorActivation } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IFileEditorInput, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, IEditorInputFactoryRegistry, Extensions as EditorExtensions, IFileInputFactory, EditorInput, SideBySideEditorInput, IEditorInputWithOptions, isEditorInputWithOptions, EditorOptions, TextEditorOptions, IEditorIdentifier, IEditorCloseEvent, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, toResource, SideBySideEditor, IRevertOptions } from 'vs/workbench/common/editor';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { DataUriEditorInput } from 'vs/workbench/common/editor/dataUriEditorInput';
import { Registry } from 'vs/platform/registry/common/platform';
import { ResourceMap } from 'vs/base/common/map';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { IFileService } from 'vs/platform/files/common/files';
import { Schemas } from 'vs/base/common/network';
import { Event, Emitter } from 'vs/base/common/event';
@@ -18,8 +17,8 @@ import { URI } from 'vs/base/common/uri';
import { basename, isEqual } from 'vs/base/common/resources';
import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput';
import { localize } from 'vs/nls';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService, IEditorGroup, GroupsOrder, IEditorReplacement, GroupChangeKind, preferredSideBySideGroupDirection, EditorsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IResourceEditor, SIDE_GROUP, IResourceEditorReplacement, IOpenEditorOverrideHandler, IVisibleEditor, IEditorService, SIDE_GROUP_TYPE, ACTIVE_GROUP_TYPE, ISaveEditorsOptions, ISaveAllEditorsOptions, IRevertAllEditorsOptions, IBaseSaveRevertAllEditorOptions } 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';
import { coalesce } from 'vs/base/common/arrays';
@@ -29,40 +28,40 @@ import { ILabelService } from 'vs/platform/label/common/label';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
type CachedEditorInput = ResourceEditorInput | IFileEditorInput | DataUriEditorInput;
type CachedEditorInput = ResourceEditorInput | IFileEditorInput;
type OpenInEditorGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE;
export class EditorService extends Disposable implements EditorServiceImpl {
_serviceBrand: undefined;
private static CACHE: ResourceMap<CachedEditorInput> = new ResourceMap<CachedEditorInput>();
private static CACHE = new ResourceMap<CachedEditorInput>();
//#region events
private readonly _onDidActiveEditorChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidActiveEditorChange: Event<void> = this._onDidActiveEditorChange.event;
private readonly _onDidActiveEditorChange = this._register(new Emitter<void>());
readonly onDidActiveEditorChange = this._onDidActiveEditorChange.event;
private readonly _onDidVisibleEditorsChange: Emitter<void> = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange: Event<void> = this._onDidVisibleEditorsChange.event;
private readonly _onDidVisibleEditorsChange = this._register(new Emitter<void>());
readonly onDidVisibleEditorsChange = this._onDidVisibleEditorsChange.event;
private readonly _onDidCloseEditor: Emitter<IEditorCloseEvent> = this._register(new Emitter<IEditorCloseEvent>());
readonly onDidCloseEditor: Event<IEditorCloseEvent> = this._onDidCloseEditor.event;
private readonly _onDidCloseEditor = this._register(new Emitter<IEditorCloseEvent>());
readonly onDidCloseEditor = this._onDidCloseEditor.event;
private readonly _onDidOpenEditorFail: Emitter<IEditorIdentifier> = this._register(new Emitter<IEditorIdentifier>());
readonly onDidOpenEditorFail: Event<IEditorIdentifier> = this._onDidOpenEditorFail.event;
private readonly _onDidOpenEditorFail = this._register(new Emitter<IEditorIdentifier>());
readonly onDidOpenEditorFail = this._onDidOpenEditorFail.event;
//#endregion
private fileInputFactory: IFileInputFactory;
private openEditorHandlers: IOpenEditorOverrideHandler[] = [];
private lastActiveEditor: IEditorInput | null = null;
private lastActiveGroupId: GroupIdentifier | null = null;
private lastActiveEditor: IEditorInput | undefined = undefined;
private lastActiveGroupId: GroupIdentifier | undefined = undefined;
constructor(
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService,
@IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService private readonly untitledTextEditorService: IUntitledTextEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILabelService private readonly labelService: ILabelService,
@IFileService private readonly fileService: IFileService,
@@ -76,6 +75,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
private registerListeners(): void {
// Editor & group changes
this.editorGroupService.whenRestored.then(() => this.onEditorsRestored());
this.editorGroupService.onDidActiveGroupChange(group => this.handleActiveEditorChange(group));
this.editorGroupService.onDidAddGroup(group => this.registerGroupListeners(group as IEditorGroupView));
@@ -88,7 +89,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
// Fire initial set of editor events if there is an active editor
if (this.activeEditor) {
this.doEmitActiveEditorChangeEvent();
this.doHandleActiveEditorChangeEvent();
this._onDidVisibleEditorsChange.fire();
}
}
@@ -106,15 +107,17 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return; // ignore if the editor actually did not change
}
this.doEmitActiveEditorChangeEvent();
this.doHandleActiveEditorChangeEvent();
}
private doEmitActiveEditorChangeEvent(): void {
private doHandleActiveEditorChangeEvent(): void {
// Remember as last active
const activeGroup = this.editorGroupService.activeGroup;
this.lastActiveGroupId = activeGroup.id;
this.lastActiveEditor = activeGroup.activeEditor;
this.lastActiveEditor = withNullAsUndefined(activeGroup.activeEditor);
// Fire event to outside parties
this._onDidActiveEditorChange.fire();
}
@@ -221,7 +224,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
//#region openEditor()
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: OpenInEditorGroup): Promise<IEditor | undefined>;
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: OpenInEditorGroup): Promise<ITextEditor | undefined>;
openEditor(editor: IResourceInput | IUntitledTextResourceInput, group?: OpenInEditorGroup): Promise<ITextEditor | undefined>;
openEditor(editor: IResourceDiffInput, group?: OpenInEditorGroup): Promise<ITextDiffEditor | undefined>;
openEditor(editor: IResourceSideBySideInput, group?: OpenInEditorGroup): Promise<ITextSideBySideEditor | undefined>;
async openEditor(editor: IEditorInput | IResourceEditor, optionsOrGroup?: IEditorOptions | ITextEditorOptions | OpenInEditorGroup, group?: OpenInEditorGroup): Promise<IEditor | undefined> {
@@ -362,7 +365,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return neighbourGroup;
}
private toOptions(options?: IEditorOptions | EditorOptions): EditorOptions {
private toOptions(options?: IEditorOptions | ITextEditorOptions | EditorOptions): EditorOptions {
if (!options || options instanceof EditorOptions) {
return options as EditorOptions;
}
@@ -424,7 +427,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
//#region isOpen()
isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput): boolean {
isOpen(editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): boolean {
return !!this.doGetOpened(editor);
}
@@ -432,13 +435,13 @@ export class EditorService extends Disposable implements EditorServiceImpl {
//#region getOpend()
getOpened(editor: IResourceInput | IUntitledResourceInput): IEditorInput | undefined {
getOpened(editor: IResourceInput | IUntitledTextResourceInput): IEditorInput | undefined {
return this.doGetOpened(editor);
}
private doGetOpened(editor: IEditorInput | IResourceInput | IUntitledResourceInput): IEditorInput | undefined {
private doGetOpened(editor: IEditorInput | IResourceInput | IUntitledTextResourceInput): IEditorInput | undefined {
if (!(editor instanceof EditorInput)) {
const resourceInput = editor as IResourceInput | IUntitledResourceInput;
const resourceInput = editor as IResourceInput | IUntitledTextResourceInput;
if (!resourceInput.resource) {
return undefined; // we need a resource at least
}
@@ -462,7 +465,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
continue; // need a resource to compare with
}
const resourceInput = editor as IResourceInput | IUntitledResourceInput;
const resourceInput = editor as IResourceInput | IUntitledTextResourceInput;
if (resourceInput.resource && isEqual(resource, resourceInput.resource)) {
return editorInGroup;
}
@@ -484,17 +487,20 @@ export class EditorService extends Disposable implements EditorServiceImpl {
editors.forEach(replaceEditorArg => {
if (replaceEditorArg.editor instanceof EditorInput) {
typedEditors.push(replaceEditorArg as IEditorReplacement);
} else {
const editor = replaceEditorArg.editor as IResourceEditor;
const replacement = replaceEditorArg.replacement as IResourceEditor;
const typedEditor = this.createInput(editor);
const typedReplacement = this.createInput(replacement);
const replacementArg = replaceEditorArg as IEditorReplacement;
typedEditors.push({
editor: typedEditor,
replacement: typedReplacement,
options: this.toOptions(replacement.options)
editor: replacementArg.editor,
replacement: replacementArg.replacement,
options: this.toOptions(replacementArg.options)
});
} else {
const replacementArg = replaceEditorArg as IResourceEditorReplacement;
typedEditors.push({
editor: this.createInput(replacementArg.editor),
replacement: this.createInput(replacementArg.replacement),
options: this.toOptions(replacementArg.replacement.options)
});
}
});
@@ -568,17 +574,17 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
// Untitled file support
const untitledInput = input as IUntitledResourceInput;
const untitledInput = input as IUntitledTextResourceInput;
if (untitledInput.forceUntitled || !untitledInput.resource || (untitledInput.resource && untitledInput.resource.scheme === Schemas.untitled)) {
return this.untitledEditorService.createOrGet(untitledInput.resource, untitledInput.mode, untitledInput.contents, untitledInput.encoding);
return this.untitledTextEditorService.createOrGet(untitledInput.resource, untitledInput.mode, untitledInput.contents, untitledInput.encoding);
}
// Resource Editor Support
const resourceInput = input as IResourceInput;
if (resourceInput.resource instanceof URI) {
let label = resourceInput.label;
if (!label && resourceInput.resource.scheme !== Schemas.data) {
label = basename(resourceInput.resource); // derive the label from the path (but not for data URIs)
if (!label) {
label = basename(resourceInput.resource); // derive the label from the path
}
return this.createOrGet(resourceInput.resource, this.instantiationService, label, resourceInput.description, resourceInput.encoding, resourceInput.mode, resourceInput.forceFile) as EditorInput;
@@ -602,7 +608,7 @@ export class EditorService extends Disposable implements EditorServiceImpl {
if (mode) {
input.setPreferredMode(mode);
}
} else if (!(input instanceof DataUriEditorInput)) {
} else {
if (encoding) {
input.setPreferredEncoding(encoding);
}
@@ -621,11 +627,6 @@ export class EditorService extends Disposable implements EditorServiceImpl {
input = this.fileInputFactory.createFileInput(resource, encoding, mode, instantiationService);
}
// Data URI
else if (resource.scheme === Schemas.data) {
input = instantiationService.createInstance(DataUriEditorInput, label, description, resource);
}
// Resource
else {
input = instantiationService.createInstance(ResourceEditorInput, label, description, resource, mode);
@@ -644,8 +645,8 @@ export class EditorService extends Disposable implements EditorServiceImpl {
return undefined;
}
// Do not try to extract any paths from simple untitled editors
if (res.scheme === Schemas.untitled && !this.untitledEditorService.hasAssociatedFilePath(res)) {
// Do not try to extract any paths from simple untitled text editors
if (res.scheme === Schemas.untitled && !this.untitledTextEditorService.hasAssociatedFilePath(res)) {
return input.getName();
}
@@ -654,6 +655,101 @@ export class EditorService extends Disposable implements EditorServiceImpl {
}
//#endregion
//#region save/revert
async save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean> {
// Convert to array
if (!Array.isArray(editors)) {
editors = [editors];
}
// Split editors up into a bucket that is saved in parallel
// and sequentially. Unless "Save As", all non-untitled editors
// can be saved in parallel to speed up the operation. Remaining
// editors are potentially bringing up some UI and thus run
// sequentially.
const editorsToSaveParallel: IEditorIdentifier[] = [];
const editorsToSaveAsSequentially: IEditorIdentifier[] = [];
if (options?.saveAs) {
editorsToSaveAsSequentially.push(...editors);
} else {
for (const { groupId, editor } of editors) {
if (editor.isUntitled()) {
editorsToSaveAsSequentially.push({ groupId, editor });
} else {
editorsToSaveParallel.push({ groupId, editor });
}
}
}
// Editors to save in parallel
await Promise.all(editorsToSaveParallel.map(({ groupId, editor }) => {
// Use save as a hint to pin the editor
this.editorGroupService.getGroup(groupId)?.pinEditor(editor);
// Save
return editor.save(groupId, options);
}));
// Editors to save sequentially
for (const { groupId, editor } of editorsToSaveAsSequentially) {
if (editor.isDisposed()) {
continue; // might have been disposed from from the save already
}
const result = options?.saveAs ? await editor.saveAs(groupId, options) : await editor.save(groupId, options);
if (!result) {
return false; // failed or cancelled, abort
}
}
return true;
}
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean> {
return this.save(this.getAllDirtyEditors(options), options);
}
async revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean> {
// Convert to array
if (!Array.isArray(editors)) {
editors = [editors];
}
const result = await Promise.all(editors.map(async ({ groupId, editor }) => {
// Use revert as a hint to pin the editor
this.editorGroupService.getGroup(groupId)?.pinEditor(editor);
return editor.revert(options);
}));
return result.every(success => !!success);
}
async revertAll(options?: IRevertAllEditorsOptions): Promise<boolean> {
return this.revert(this.getAllDirtyEditors(options), options);
}
private getAllDirtyEditors(options?: IBaseSaveRevertAllEditorOptions): IEditorIdentifier[] {
const editors: IEditorIdentifier[] = [];
for (const group of this.editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
for (const editor of group.getEditors(EditorsOrder.MOST_RECENTLY_ACTIVE)) {
if (editor.isDirty() && (!editor.isUntitled() || !!options?.includeUntitled)) {
editors.push({ groupId: group.id, editor });
}
}
}
return editors;
}
//#endregion
}
export interface IEditorOpenHandler {
@@ -674,7 +770,7 @@ export class DelegatingEditorService extends EditorService {
constructor(
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@IInstantiationService instantiationService: IInstantiationService,
@ILabelService labelService: ILabelService,
@IFileService fileService: IFileService,
@@ -682,7 +778,7 @@ export class DelegatingEditorService extends EditorService {
) {
super(
editorGroupService,
untitledEditorService,
untitledTextEditorService,
instantiationService,
labelService,
fileService,

View File

@@ -427,11 +427,6 @@ export interface IEditorGroup {
*/
readonly editors: ReadonlyArray<IEditorInput>;
/**
* Returns the editor at a specific index of the group.
*/
getEditor(index: number): IEditorInput | undefined;
/**
* Get all editors that are currently opened in the group optionally
* sorted by being most recent active. Will sort by sequential appearance
@@ -439,6 +434,11 @@ export interface IEditorGroup {
*/
getEditors(order?: EditorsOrder): ReadonlyArray<IEditorInput>;
/**
* Returns the editor at a specific index of the group.
*/
getEditorByIndex(index: number): IEditorInput | undefined;
/**
* Returns the index of the editor in the group or -1 if not opened.
*/

View File

@@ -5,7 +5,7 @@
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IResourceInput, IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor } from 'vs/workbench/common/editor';
import { IEditorInput, IEditor, GroupIdentifier, IEditorInputWithOptions, IUntitledTextResourceInput, IResourceDiffInput, IResourceSideBySideInput, ITextEditor, ITextDiffEditor, ITextSideBySideEditor, IEditorIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
import { Event } from 'vs/base/common/event';
import { IEditor as ICodeEditor } from 'vs/editor/common/editorCommon';
import { IEditorGroup, IEditorReplacement } from 'vs/workbench/services/editor/common/editorGroupsService';
@@ -13,7 +13,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
export const IEditorService = createDecorator<IEditorService>('editorService');
export type IResourceEditor = IResourceInput | IUntitledResourceInput | IResourceDiffInput | IResourceSideBySideInput;
export type IResourceEditor = IResourceInput | IUntitledTextResourceInput | IResourceDiffInput | IResourceSideBySideInput;
export interface IResourceEditorReplacement {
editor: IResourceEditor;
@@ -44,6 +44,26 @@ export interface IVisibleEditor extends IEditor {
group: IEditorGroup;
}
export interface ISaveEditorsOptions extends ISaveOptions {
/**
* If true, will ask for a location of the editor to save to.
*/
saveAs?: boolean;
}
export interface IBaseSaveRevertAllEditorOptions {
/**
* Wether to include untitled editors as well.
*/
includeUntitled?: boolean;
}
export interface ISaveAllEditorsOptions extends ISaveEditorsOptions, IBaseSaveRevertAllEditorOptions { }
export interface IRevertAllEditorsOptions extends IRevertOptions, IBaseSaveRevertAllEditorOptions { }
export interface IEditorService {
_serviceBrand: undefined;
@@ -121,7 +141,7 @@ export interface IEditorService {
* opened to be active.
*/
openEditor(editor: IEditorInput, options?: IEditorOptions | ITextEditorOptions, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<IEditor | undefined>;
openEditor(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextEditor | undefined>;
openEditor(editor: IResourceInput | IUntitledTextResourceInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextEditor | undefined>;
openEditor(editor: IResourceDiffInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextDiffEditor | undefined>;
openEditor(editor: IResourceSideBySideInput, group?: IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE): Promise<ITextSideBySideEditor | undefined>;
@@ -158,7 +178,7 @@ export interface IEditorService {
*
* @param group optional to specify a group to check for the editor being opened
*/
isOpen(editor: IEditorInput | IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): boolean;
isOpen(editor: IEditorInput | IResourceInput | IUntitledTextResourceInput, group?: IEditorGroup | GroupIdentifier): boolean;
/**
* Get the actual opened editor input in any or a specific editor group based on the resource.
@@ -167,7 +187,7 @@ export interface IEditorService {
*
* @param group optional to specify a group to check for the editor
*/
getOpened(editor: IResourceInput | IUntitledResourceInput, group?: IEditorGroup | GroupIdentifier): IEditorInput | undefined;
getOpened(editor: IResourceInput | IUntitledTextResourceInput, group?: IEditorGroup | GroupIdentifier): IEditorInput | undefined;
/**
* Allows to override the opening of editors by installing a handler that will
@@ -184,5 +204,25 @@ export interface IEditorService {
/**
* Converts a lightweight input to a workbench editor input.
*/
createInput(input: IResourceEditor): IEditorInput | null;
createInput(input: IResourceEditor): IEditorInput;
/**
* Save the provided list of editors.
*/
save(editors: IEditorIdentifier | IEditorIdentifier[], options?: ISaveEditorsOptions): Promise<boolean>;
/**
* Save all editors.
*/
saveAll(options?: ISaveAllEditorsOptions): Promise<boolean>;
/**
* Reverts the provided list of editors.
*/
revert(editors: IEditorIdentifier | IEditorIdentifier[], options?: IRevertOptions): Promise<boolean>;
/**
* Reverts all editors.
*/
revertAll(options?: IRevertAllEditorsOptions): Promise<boolean>;
}

View File

@@ -78,7 +78,7 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
}
(Registry.as<IEditorInputFactoryRegistry>(EditorExtensions.EditorInputFactories)).registerEditorInputFactory('testEditorInputForGroupsService', TestEditorInputFactory);
(Registry.as<IEditorRegistry>(Extensions.Editors)).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForGroupsService', 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)]);
(Registry.as<IEditorRegistry>(Extensions.Editors)).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForGroupsService', 'My Test File Editor'), [new SyncDescriptor(TestEditorInput)]);
}
registerTestEditorInput();
@@ -440,8 +440,8 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
assert.equal(editorWillOpenCounter, 2);
assert.equal(editorDidOpenCounter, 2);
assert.equal(activeEditorChangeCounter, 1);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
assert.equal(group.getIndexOfEditor(input), 0);
assert.equal(group.getIndexOfEditor(inputInactive), 1);
@@ -491,8 +491,8 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
await group.closeEditors([input, inputInactive]);
assert.equal(group.isEmpty, true);
@@ -510,13 +510,13 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
assert.equal(group.count, 3);
assert.equal(group.getEditor(0), input1);
assert.equal(group.getEditor(1), input2);
assert.equal(group.getEditor(2), input3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ except: input2 });
assert.equal(group.count, 1);
assert.equal(group.getEditor(0), input2);
assert.equal(group.getEditorByIndex(0), input2);
part.dispose();
});
@@ -531,9 +531,9 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
assert.equal(group.count, 3);
assert.equal(group.getEditor(0), input1);
assert.equal(group.getEditor(1), input2);
assert.equal(group.getEditor(2), input3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ savedOnly: true });
assert.equal(group.count, 0);
@@ -551,14 +551,14 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
assert.equal(group.count, 3);
assert.equal(group.getEditor(0), input1);
assert.equal(group.getEditor(1), input2);
assert.equal(group.getEditor(2), input3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ direction: CloseDirection.RIGHT, except: input2 });
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input1);
assert.equal(group.getEditor(1), input2);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
part.dispose();
});
@@ -573,14 +573,14 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input1, options: { pinned: true } }, { editor: input2, options: { pinned: true } }, { editor: input3 }]);
assert.equal(group.count, 3);
assert.equal(group.getEditor(0), input1);
assert.equal(group.getEditor(1), input2);
assert.equal(group.getEditor(2), input3);
assert.equal(group.getEditorByIndex(0), input1);
assert.equal(group.getEditorByIndex(1), input2);
assert.equal(group.getEditorByIndex(2), input3);
await group.closeEditors({ direction: CloseDirection.LEFT, except: input2 });
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input2);
assert.equal(group.getEditor(1), input3);
assert.equal(group.getEditorByIndex(0), input2);
assert.equal(group.getEditorByIndex(1), input3);
part.dispose();
});
@@ -594,8 +594,8 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
await group.closeAllEditors();
assert.equal(group.isEmpty, true);
@@ -620,12 +620,12 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
group.moveEditor(inputInactive, group, { index: 0 });
assert.equal(editorMoveCounter, 1);
assert.equal(group.getEditor(0), inputInactive);
assert.equal(group.getEditor(1), input);
assert.equal(group.getEditorByIndex(0), inputInactive);
assert.equal(group.getEditorByIndex(1), input);
editorGroupChangeListener.dispose();
part.dispose();
});
@@ -642,13 +642,13 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
group.moveEditor(inputInactive, rightGroup, { index: 0 });
assert.equal(group.count, 1);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(rightGroup.count, 1);
assert.equal(rightGroup.getEditor(0), inputInactive);
assert.equal(rightGroup.getEditorByIndex(0), inputInactive);
part.dispose();
});
@@ -664,14 +664,14 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditors([{ editor: input, options: { pinned: true } }, { editor: inputInactive }]);
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
group.copyEditor(inputInactive, rightGroup, { index: 0 });
assert.equal(group.count, 2);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditor(1), inputInactive);
assert.equal(group.getEditorByIndex(0), input);
assert.equal(group.getEditorByIndex(1), inputInactive);
assert.equal(rightGroup.count, 1);
assert.equal(rightGroup.getEditor(0), inputInactive);
assert.equal(rightGroup.getEditorByIndex(0), inputInactive);
part.dispose();
});
@@ -685,11 +685,11 @@ suite.skip('EditorGroupsService', () => { // {{SQL CARBON EDIT}} skip suite
await group.openEditor(input);
assert.equal(group.count, 1);
assert.equal(group.getEditor(0), input);
assert.equal(group.getEditorByIndex(0), input);
await group.replaceEditors([{ editor: input, replacement: inputInactive }]);
assert.equal(group.count, 1);
assert.equal(group.getEditor(0), inputInactive);
assert.equal(group.getEditorByIndex(0), inputInactive);
part.dispose();
});

View File

@@ -7,7 +7,7 @@ import * as assert from 'assert';
import { IEditorModel, EditorActivation } from 'vs/platform/editor/common/editor';
import { URI } from 'vs/base/common/uri';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput } from 'vs/workbench/common/editor';
import { EditorInput, EditorOptions, IFileEditorInput, IEditorInput, GroupIdentifier, ISaveOptions, IRevertOptions } from 'vs/workbench/common/editor';
import { workbenchInstantiationService, TestStorageService } from 'vs/workbench/test/workbenchTestServices';
import { ResourceEditorInput } from 'vs/workbench/common/editor/resourceEditorInput';
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
@@ -22,7 +22,7 @@ import { IEditorRegistry, EditorDescriptor, Extensions } from 'vs/workbench/brow
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { Registry } from 'vs/platform/registry/common/platform';
import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput';
import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput';
import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput';
import { EditorServiceImpl } from 'vs/workbench/browser/parts/editor/editor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { timeout } from 'vs/base/common/async';
@@ -30,7 +30,7 @@ import { toResource } from 'vs/base/test/common/utils';
import { IFileService } from 'vs/platform/files/common/files';
import { Disposable } from 'vs/base/common/lifecycle';
import { ModesRegistry } from 'vs/editor/common/modes/modesRegistry';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel';
import { NullFileSystemProvider } from 'vs/platform/files/test/common/nullFileSystemProvider';
export class TestEditorControl extends BaseEditor {
@@ -50,11 +50,15 @@ export class TestEditorControl extends BaseEditor {
export class TestEditorInput extends EditorInput implements IFileEditorInput {
public gotDisposed = false;
public gotSaved = false;
public gotSavedAs = false;
public gotReverted = false;
public dirty = false;
private fails = false;
constructor(private resource: URI) { super(); }
getTypeId() { return 'testEditorInputForEditorService'; }
resolve(): Promise<IEditorModel> { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); }
resolve(): Promise<IEditorModel | null> { return !this.fails ? Promise.resolve(null) : Promise.reject(new Error('fails')); }
matches(other: TestEditorInput): boolean { return other && other.resource && this.resource.toString() === other.resource.toString() && other instanceof TestEditorInput; }
setEncoding(encoding: string) { }
getEncoding() { return undefined; }
@@ -66,6 +70,26 @@ export class TestEditorInput extends EditorInput implements IFileEditorInput {
setFailToOpen(): void {
this.fails = true;
}
save(groupId: GroupIdentifier, options?: ISaveOptions): Promise<boolean> {
this.gotSaved = true;
return Promise.resolve(true);
}
saveAs(groupId: GroupIdentifier, options?: ISaveOptions): Promise<boolean> {
this.gotSavedAs = true;
return Promise.resolve(true);
}
revert(options?: IRevertOptions): Promise<boolean> {
this.gotReverted = true;
this.gotSaved = false;
this.gotSavedAs = false;
return Promise.resolve(true);
}
isDirty(): boolean {
return this.dirty;
}
isReadonly(): boolean {
return false;
}
dispose(): void {
super.dispose();
this.gotDisposed = true;
@@ -83,7 +107,7 @@ class FileServiceProvider extends Disposable {
suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
function registerTestEditorInput(): void {
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(new EditorDescriptor(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)]);
Registry.as<IEditorRegistry>(Extensions.Editors).registerEditor(EditorDescriptor.create(TestEditorControl, 'MyTestEditorForEditorService', 'My Test Editor For Next Editor Service'), [new SyncDescriptor(TestEditorInput)]);
}
registerTestEditorInput();
@@ -270,36 +294,36 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
// Untyped Input (untitled)
input = service.createInput({ options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert(input instanceof UntitledTextEditorInput);
// Untyped Input (untitled with contents)
input = service.createInput({ contents: 'Hello Untitled', options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
let model = await input.resolve() as UntitledEditorModel;
assert(input instanceof UntitledTextEditorInput);
let model = await input.resolve() as UntitledTextEditorModel;
assert.equal(model.textEditorModel!.getValue(), 'Hello Untitled');
// Untyped Input (untitled with mode)
input = service.createInput({ mode, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
model = await input.resolve() as UntitledEditorModel;
assert(input instanceof UntitledTextEditorInput);
model = await input.resolve() as UntitledTextEditorModel;
assert.equal(model.getMode(), mode);
// Untyped Input (untitled with file path)
input = service.createInput({ resource: URI.file('/some/path.txt'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
assert(input instanceof UntitledTextEditorInput);
assert.ok((input as UntitledTextEditorInput).hasAssociatedFilePath);
// Untyped Input (untitled with untitled resource)
input = service.createInput({ resource: URI.parse('untitled://Untitled-1'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok(!(input as UntitledEditorInput).hasAssociatedFilePath);
assert(input instanceof UntitledTextEditorInput);
assert.ok(!(input as UntitledTextEditorInput).hasAssociatedFilePath);
// Untyped Input (untitled with custom resource)
const provider = instantiationService.createInstance(FileServiceProvider, 'untitled-custom');
input = service.createInput({ resource: URI.parse('untitled-custom://some/path'), forceUntitled: true, options: { selection: { startLineNumber: 1, startColumn: 1 } } });
assert(input instanceof UntitledEditorInput);
assert.ok((input as UntitledEditorInput).hasAssociatedFilePath);
assert(input instanceof UntitledTextEditorInput);
assert.ok((input as UntitledTextEditorInput).hasAssociatedFilePath);
provider.dispose();
@@ -686,4 +710,45 @@ suite.skip('EditorService', () => { // {{SQL CARBON EDIT}} skip suite
let failingEditor = await service.openEditor(failingInput);
assert.ok(!failingEditor);
});
test('save, saveAll, revertAll', async function () {
const partInstantiator = workbenchInstantiationService();
const part = partInstantiator.createInstance(EditorPart);
part.create(document.createElement('div'));
part.layout(400, 300);
const testInstantiationService = partInstantiator.createChild(new ServiceCollection([IEditorGroupsService, part]));
const service: IEditorService = testInstantiationService.createInstance(EditorService);
const input1 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource1-openside'));
input1.dirty = true;
const input2 = testInstantiationService.createInstance(TestEditorInput, URI.parse('my://resource2-openside'));
input2.dirty = true;
const rootGroup = part.activeGroup;
await part.whenRestored;
await service.openEditor(input1, { pinned: true });
await service.openEditor(input2, { pinned: true });
await service.save({ groupId: rootGroup.id, editor: input1 });
assert.equal(input1.gotSaved, true);
await service.save({ groupId: rootGroup.id, editor: input1 }, { saveAs: true });
assert.equal(input1.gotSavedAs, true);
await service.revertAll();
assert.equal(input1.gotReverted, true);
await service.saveAll();
assert.equal(input1.gotSaved, true);
assert.equal(input2.gotSaved, true);
await service.saveAll({ saveAs: true });
assert.equal(input1.gotSavedAs, true);
assert.equal(input2.gotSavedAs, true);
});
});

View File

@@ -9,7 +9,7 @@ import { IProcessEnvironment } from 'vs/base/common/platform';
import { joinPath } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { BACKUPS, IDebugParams, IExtensionHostDebugParams } from 'vs/platform/environment/common/environment';
import { BACKUPS, IExtensionHostDebugParams } from 'vs/platform/environment/common/environment';
import { LogLevel } from 'vs/platform/log/common/log';
import { IPath, IPathsToWaitFor, IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { ISingleFolderWorkspaceIdentifier, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
@@ -17,104 +17,294 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
import product from 'vs/platform/product/common/product';
import { serializableToMap } from 'vs/base/common/map';
import { memoize } from 'vs/base/common/decorators';
// TODO@ben remove properties that are node/electron only
export class BrowserWindowConfiguration implements IWindowConfiguration {
constructor(
private readonly options: IBrowserWorkbenchEnvironmentConstructionOptions,
private readonly payload: Map<string, string> | undefined,
private readonly environment: IWorkbenchEnvironmentService
) { }
//#region PROPERLY CONFIGURED IN DESKTOP + WEB
@memoize
get sessionId(): string { return generateUuid(); }
@memoize
get remoteAuthority(): string | undefined { return this.options.remoteAuthority; }
@memoize
get connectionToken(): string | undefined { return this.options.connectionToken || this.getCookieValue('vscode-tkn'); }
@memoize
get backupWorkspaceResource(): URI { return joinPath(this.environment.backupHome, this.options.workspaceId); }
@memoize
get filesToOpenOrCreate(): IPath[] | undefined {
if (this.payload) {
const fileToOpen = this.payload.get('openFile');
if (fileToOpen) {
return [{ fileUri: URI.parse(fileToOpen) }];
}
}
return undefined;
}
// Currently unsupported in web
get filesToDiff(): IPath[] | undefined { return undefined; }
//#endregion
//#region TODO MOVE TO NODE LAYER
_!: any[];
machineId!: string;
windowId!: number;
logLevel!: LogLevel;
mainPid!: number;
logLevel!: LogLevel;
appRoot!: string;
execPath!: string;
isInitialStartup?: boolean;
userEnv!: IProcessEnvironment;
backupPath?: string;
nodeCachedDataDir?: string;
backupPath?: string;
backupWorkspaceResource?: URI;
userEnv!: IProcessEnvironment;
workspace?: IWorkspaceIdentifier;
folderUri?: ISingleFolderWorkspaceIdentifier;
remoteAuthority?: string;
connectionToken?: string;
zoomLevel?: number;
fullscreen?: boolean;
maximized?: boolean;
highContrast?: boolean;
frameless?: boolean;
accessibilitySupport?: boolean;
partsSplashPath?: string;
perfStartTime?: number;
perfAppReady?: number;
perfWindowLoadTime?: number;
isInitialStartup?: boolean;
perfEntries!: ExportData;
filesToOpenOrCreate?: IPath[];
filesToDiff?: IPath[];
filesToWait?: IPathsToWaitFor;
termProgram?: string;
//#endregion
private getCookieValue(name: string): string | undefined {
const m = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)'); // See https://stackoverflow.com/a/25490531
return m ? m.pop() : undefined;
}
}
interface IBrowserWorkbenchEnvironemntConstructionOptions extends IWorkbenchConstructionOptions {
interface IBrowserWorkbenchEnvironmentConstructionOptions extends IWorkbenchConstructionOptions {
workspaceId: string;
logsPath: URI;
}
interface IExtensionHostDebugEnvironment {
params: IExtensionHostDebugParams;
isExtensionDevelopment: boolean;
extensionDevelopmentLocationURI: URI[];
extensionTestsLocationURI?: URI;
}
export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironmentService {
_serviceBrand: undefined;
readonly configuration: IWindowConfiguration = new BrowserWindowConfiguration();
//#region PROPERLY CONFIGURED IN DESKTOP + WEB
constructor(readonly options: IBrowserWorkbenchEnvironemntConstructionOptions) {
this.args = { _: [] };
this.logsPath = options.logsPath.path;
this.logFile = joinPath(options.logsPath, 'window.log');
this.appRoot = '/web/';
this.appNameLong = 'Azure Data Studio - Web'; // {{SQL CARBON EDIT}} vscode to ads
@memoize
get isBuilt(): boolean { return !!product.commit; }
this.configuration.remoteAuthority = options.remoteAuthority;
this.configuration.machineId = generateUuid();
this.userRoamingDataHome = URI.file('/User').with({ scheme: Schemas.userData });
this.settingsResource = joinPath(this.userRoamingDataHome, 'settings.json');
this.settingsSyncPreviewResource = joinPath(this.userRoamingDataHome, '.settings.json');
this.userDataSyncLogResource = joinPath(options.logsPath, 'userDataSync.log');
this.keybindingsResource = joinPath(this.userRoamingDataHome, 'keybindings.json');
this.keyboardLayoutResource = joinPath(this.userRoamingDataHome, 'keyboardLayout.json');
this.argvResource = joinPath(this.userRoamingDataHome, 'argv.json');
this.backupHome = joinPath(this.userRoamingDataHome, BACKUPS);
this.untitledWorkspacesHome = joinPath(this.userRoamingDataHome, 'Workspaces');
this.configuration.backupWorkspaceResource = joinPath(this.backupHome, options.workspaceId);
this.configuration.connectionToken = options.connectionToken || getCookieValue('vscode-tkn');
@memoize
get logsPath(): string { return this.options.logsPath.path; }
this.debugExtensionHost = {
port: null,
break: false
@memoize
get logFile(): URI { return joinPath(this.options.logsPath, 'window.log'); }
@memoize
get userRoamingDataHome(): URI { return URI.file('/User').with({ scheme: Schemas.userData }); }
@memoize
get settingsResource(): URI { return joinPath(this.userRoamingDataHome, 'settings.json'); }
@memoize
get settingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.settings.json'); }
@memoize
get keybindingsSyncPreviewResource(): URI { return joinPath(this.userRoamingDataHome, '.keybindings.json'); }
@memoize
get userDataSyncLogResource(): URI { return joinPath(this.options.logsPath, 'userDataSync.log'); }
@memoize
get keybindingsResource(): URI { return joinPath(this.userRoamingDataHome, 'keybindings.json'); }
@memoize
get keyboardLayoutResource(): URI { return joinPath(this.userRoamingDataHome, 'keyboardLayout.json'); }
@memoize
get backupHome(): URI { return joinPath(this.userRoamingDataHome, BACKUPS); }
@memoize
get untitledWorkspacesHome(): URI { return joinPath(this.userRoamingDataHome, 'Workspaces'); }
private _extensionHostDebugEnvironment: IExtensionHostDebugEnvironment | undefined = undefined;
get debugExtensionHost(): IExtensionHostDebugParams {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
}
return this._extensionHostDebugEnvironment.params;
}
get isExtensionDevelopment(): boolean {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
}
return this._extensionHostDebugEnvironment.isExtensionDevelopment;
}
get extensionDevelopmentLocationURI(): URI[] {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
}
return this._extensionHostDebugEnvironment.extensionDevelopmentLocationURI;
}
get extensionTestsLocationURI(): URI | undefined {
if (!this._extensionHostDebugEnvironment) {
this._extensionHostDebugEnvironment = this.resolveExtensionHostDebugEnvironment();
}
return this._extensionHostDebugEnvironment.extensionTestsLocationURI;
}
@memoize
get webviewExternalEndpoint(): string {
// TODO: get fallback from product.json
return (this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}').replace('{{commit}}', product.commit || '0d728c31ebdf03869d2687d9be0b017667c9ff37');
}
@memoize
get webviewResourceRoot(): string {
return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`;
}
@memoize
get webviewCspSource(): string {
return this.webviewExternalEndpoint.replace('{{uuid}}', '*');
}
// Currently not configurable in web
get disableExtensions() { return false; }
get extensionsPath(): string | undefined { return undefined; }
get verbose(): boolean { return false; }
get disableUpdates(): boolean { return false; }
get logExtensionHostCommunication(): boolean { return false; }
//#endregion
//#region TODO MOVE TO NODE LAYER
private _configuration: IWindowConfiguration | undefined = undefined;
get configuration(): IWindowConfiguration {
if (!this._configuration) {
this._configuration = new BrowserWindowConfiguration(this.options, this.payload, this);
}
return this._configuration;
}
args = { _: [] };
wait!: boolean;
status!: boolean;
log?: string;
mainIPCHandle!: string;
sharedIPCHandle!: string;
nodeCachedDataDir?: string;
argvResource!: URI;
disableCrashReporter!: boolean;
driverHandle?: string;
driverVerbose!: boolean;
installSourcePath!: string;
builtinExtensionsPath!: string;
globalStorageHome!: string;
workspaceStorageHome!: string;
backupWorkspacesPath!: string;
machineSettingsHome!: URI;
machineSettingsResource!: URI;
userHome!: string;
userDataPath!: string;
appRoot!: string;
appSettingsHome!: URI;
execPath!: string;
cliPath!: string;
//#endregion
//#region TODO ENABLE IN WEB
galleryMachineIdResource?: URI;
//#endregion
private payload: Map<string, string> | undefined;
constructor(readonly options: IBrowserWorkbenchEnvironmentConstructionOptions) {
if (options.workspaceProvider && Array.isArray(options.workspaceProvider.payload)) {
this.payload = serializableToMap(options.workspaceProvider.payload);
}
}
private resolveExtensionHostDebugEnvironment(): IExtensionHostDebugEnvironment {
const extensionHostDebugEnvironment: IExtensionHostDebugEnvironment = {
params: {
port: null,
break: false
},
isExtensionDevelopment: false,
extensionDevelopmentLocationURI: []
};
// Fill in selected extra environmental properties
if (options.workspaceProvider && Array.isArray(options.workspaceProvider.payload)) {
const environment = serializableToMap(options.workspaceProvider.payload);
for (const [key, value] of environment) {
if (this.payload) {
for (const [key, value] of this.payload) {
switch (key) {
case 'extensionDevelopmentPath':
this.extensionDevelopmentLocationURI = [URI.parse(value)];
this.isExtensionDevelopment = true;
extensionHostDebugEnvironment.extensionDevelopmentLocationURI = [URI.parse(value)];
extensionHostDebugEnvironment.isExtensionDevelopment = true;
break;
case 'extensionTestsPath':
extensionHostDebugEnvironment.extensionTestsLocationURI = URI.parse(value);
break;
case 'debugId':
this.debugExtensionHost.debugId = value;
extensionHostDebugEnvironment.params.debugId = value;
break;
case 'inspect-brk-extensions':
this.debugExtensionHost.port = parseInt(value);
this.debugExtensionHost.break = false;
extensionHostDebugEnvironment.params.port = parseInt(value);
extensionHostDebugEnvironment.params.break = true;
break;
}
}
@@ -133,96 +323,23 @@ export class BrowserWorkbenchEnvironmentService implements IWorkbenchEnvironment
const edp = map.get('extensionDevelopmentPath');
if (edp) {
this.extensionDevelopmentLocationURI = [URI.parse(edp)];
this.isExtensionDevelopment = true;
extensionHostDebugEnvironment.extensionDevelopmentLocationURI = [URI.parse(edp)];
extensionHostDebugEnvironment.isExtensionDevelopment = true;
}
const di = map.get('debugId');
if (di) {
this.debugExtensionHost.debugId = di;
extensionHostDebugEnvironment.params.debugId = di;
}
const ibe = map.get('inspect-brk-extensions');
if (ibe) {
this.debugExtensionHost.port = parseInt(ibe);
this.debugExtensionHost.break = false;
extensionHostDebugEnvironment.params.port = parseInt(ibe);
extensionHostDebugEnvironment.params.break = false;
}
}
}
}
untitledWorkspacesHome: URI;
extensionTestsLocationURI?: URI;
args: any;
execPath!: string;
cliPath!: string;
appRoot: string;
userHome!: string;
userDataPath!: string;
appNameLong: string;
appQuality?: string;
appSettingsHome!: URI;
userRoamingDataHome: URI;
settingsResource: URI;
keybindingsResource: URI;
keyboardLayoutResource: URI;
argvResource: URI;
settingsSyncPreviewResource: URI;
userDataSyncLogResource: URI;
machineSettingsHome!: URI;
machineSettingsResource!: URI;
globalStorageHome!: string;
workspaceStorageHome!: string;
backupHome: URI;
backupWorkspacesPath!: string;
workspacesHome!: string;
isExtensionDevelopment!: boolean;
disableExtensions!: boolean | string[];
builtinExtensionsPath!: string;
extensionsPath?: string;
extensionDevelopmentLocationURI?: URI[];
extensionTestsPath?: string;
debugExtensionHost: IExtensionHostDebugParams;
debugSearch!: IDebugParams;
logExtensionHostCommunication!: boolean;
isBuilt!: boolean;
wait!: boolean;
status!: boolean;
log?: string;
logsPath: string;
verbose!: boolean;
skipReleaseNotes!: boolean;
mainIPCHandle!: string;
sharedIPCHandle!: string;
nodeCachedDataDir?: string;
installSourcePath!: string;
disableUpdates!: boolean;
disableCrashReporter!: boolean;
driverHandle?: string;
driverVerbose!: boolean;
galleryMachineIdResource?: URI;
readonly logFile: URI;
get webviewExternalEndpoint(): string {
// TODO: get fallback from product.json
return (this.options.webviewEndpoint || 'https://{{uuid}}.vscode-webview-test.com/{{commit}}')
.replace('{{commit}}', product.commit || 'c58aaab8a1cc22a7139b761166a0d4f37d41e998');
}
get webviewResourceRoot(): string {
return `${this.webviewExternalEndpoint}/vscode-resource/{{resource}}`;
}
get webviewCspSource(): string {
return this.webviewExternalEndpoint
.replace('{{uuid}}', '*');
return extensionHostDebugEnvironment;
}
}
/**
* See https://stackoverflow.com/a/25490531
*/
function getCookieValue(name: string): string | undefined {
const m = document.cookie.match('(^|[^;]+)\\s*' + name + '\\s*=\\s*([^;]+)');
return m ? m.pop() : undefined;
}

View File

@@ -5,7 +5,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkbenchConstructionOptions } from 'vs/workbench/workbench.web.api';
import { URI } from 'vs/base/common/uri';
@@ -20,14 +20,8 @@ export interface IWorkbenchEnvironmentService extends IEnvironmentService {
readonly options?: IWorkbenchConstructionOptions;
readonly logFile: URI;
readonly logExtensionHostCommunication: boolean;
readonly debugSearch: IDebugParams;
readonly webviewExternalEndpoint: string;
readonly webviewResourceRoot: string;
readonly webviewCspSource: string;
readonly skipReleaseNotes: boolean | undefined;
}

View File

@@ -3,28 +3,38 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { EnvironmentService, parseSearchPort } from 'vs/platform/environment/node/environmentService';
import { EnvironmentService } from 'vs/platform/environment/node/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { memoize } from 'vs/base/common/decorators';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/common/backup';
import { toBackupWorkspaceResource } from 'vs/workbench/services/backup/electron-browser/backup';
import { join } from 'vs/base/common/path';
import { IDebugParams } from 'vs/platform/environment/common/environment';
import product from 'vs/platform/product/common/product';
export class WorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService {
export class NativeWorkbenchEnvironmentService extends EnvironmentService implements IWorkbenchEnvironmentService {
_serviceBrand: undefined;
@memoize
get webviewExternalEndpoint(): string {
const baseEndpoint = 'https://{{uuid}}.vscode-webview-test.com/{{commit}}';
return baseEndpoint.replace('{{commit}}', product.commit || 'c58aaab8a1cc22a7139b761166a0d4f37d41e998');
return baseEndpoint.replace('{{commit}}', product.commit || '0d728c31ebdf03869d2687d9be0b017667c9ff37');
}
readonly webviewResourceRoot = 'vscode-resource://{{resource}}';
readonly webviewCspSource = 'vscode-resource:';
@memoize
get webviewResourceRoot(): string { return 'vscode-resource://{{resource}}'; }
@memoize
get webviewCspSource(): string { return 'vscode-resource:'; }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
@memoize
get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.windowId}.log`)); }
constructor(
readonly configuration: IWindowConfiguration,
@@ -35,17 +45,4 @@ export class WorkbenchEnvironmentService extends EnvironmentService implements I
this.configuration.backupWorkspaceResource = this.configuration.backupPath ? toBackupWorkspaceResource(this.configuration.backupPath, this) : undefined;
}
get skipReleaseNotes(): boolean { return !!this.args['skip-release-notes']; }
@memoize
get userRoamingDataHome(): URI { return this.appSettingsHome.with({ scheme: Schemas.userData }); }
@memoize
get logFile(): URI { return URI.file(join(this.logsPath, `renderer${this.windowId}.log`)); }
get logExtensionHostCommunication(): boolean { return !!this.args.logExtensionHostCommunication; }
@memoize
get debugSearch(): IDebugParams { return parseSearchPort(this.args, this.isBuilt); }
}

View File

@@ -15,7 +15,7 @@ import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/
import { isUndefinedOrNull } from 'vs/base/common/types';
import { ExtensionType, IExtension } from 'vs/platform/extensions/common/extensions';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IProductService } from 'vs/platform/product/common/productService';
@@ -146,12 +146,21 @@ export class ExtensionEnablementService extends Disposable implements IExtension
}
private _isDisabledByExtensionKind(extension: IExtension): boolean {
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.remoteExtensionManagementServer) {
if (!isUIExtension(extension.manifest, this.productService, this.configurationService)) {
// workspace extensions must run on the remote, but UI extensions can run on either side
const server = this.extensionManagementServerService.remoteExtensionManagementServer;
return this.extensionManagementServerService.getExtensionManagementServer(extension.location) !== server;
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
const server = this.extensionManagementServerService.getExtensionManagementServer(extension.location);
for (const extensionKind of getExtensionKind(extension.manifest, this.productService, this.configurationService)) {
if (extensionKind === 'ui') {
if (this.extensionManagementServerService.localExtensionManagementServer && this.extensionManagementServerService.localExtensionManagementServer === server) {
return false;
}
}
if (extensionKind === 'workspace') {
if (server === this.extensionManagementServerService.remoteExtensionManagementServer) {
return false;
}
}
}
return true;
}
return false;
}

View File

@@ -5,7 +5,7 @@
import { Event, EventMultiplexer } from 'vs/base/common/event';
import {
IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService
IExtensionManagementService, ILocalExtension, IGalleryExtension, InstallExtensionEvent, DidInstallExtensionEvent, IExtensionIdentifier, DidUninstallExtensionEvent, IReportedExtension, IGalleryMetadata, IExtensionGalleryService, INSTALL_ERROR_NOT_SUPPORTED
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionManagementServer, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionType, isLanguagePackExtension, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
@@ -15,7 +15,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CancellationToken } from 'vs/base/common/cancellation';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { localize } from 'vs/nls';
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { prefersExecuteOnUI, canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IProductService } from 'vs/platform/product/common/productService';
import { Schemas } from 'vs/base/common/network';
import { IDownloadService } from 'vs/platform/download/common/download';
@@ -93,7 +93,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private async uninstallInServer(extension: ILocalExtension, server: IExtensionManagementServer, force?: boolean): Promise<void> {
if (server === this.extensionManagementServerService.localExtensionManagementServer) {
const installedExtensions = await this.extensionManagementServerService.remoteExtensionManagementServer!.extensionManagementService.getInstalled(ExtensionType.User);
const dependentNonUIExtensions = installedExtensions.filter(i => !isUIExtension(i.manifest, this.productService, this.configurationService)
const dependentNonUIExtensions = installedExtensions.filter(i => !prefersExecuteOnUI(i.manifest, this.productService, this.configurationService)
&& i.manifest.extensionDependencies && i.manifest.extensionDependencies.some(id => areSameExtensions({ id }, extension.identifier)));
if (dependentNonUIExtensions.length) {
return Promise.reject(new Error(this.getDependentsErrorMessage(extension, dependentNonUIExtensions)));
@@ -152,7 +152,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
const [local] = await Promise.all(this.servers.map(server => this.installVSIX(vsix, server)));
return local;
}
if (isUIExtension(manifest, this.productService, this.configurationService)) {
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService)) {
// Install only on local server
return this.installVSIX(vsix, this.extensionManagementServerService.localExtensionManagementServer);
}
@@ -190,7 +190,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
// Install on both servers
return Promise.all(this.servers.map(server => server.extensionManagementService.installFromGallery(gallery))).then(([local]) => local);
}
if (isUIExtension(manifest, this.productService, this.configurationService)) {
if (prefersExecuteOnUI(manifest, this.productService, this.configurationService)) {
// Install only on local server
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
}
@@ -204,6 +204,15 @@ export class ExtensionManagementService extends Disposable implements IExtension
return this.extensionManagementServerService.localExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
}
if (this.extensionManagementServerService.remoteExtensionManagementServer) {
const manifest = await this.extensionGalleryService.getManifest(gallery, CancellationToken.None);
if (!manifest) {
return Promise.reject(localize('Manifest is not found', "Installing Extension {0} failed: Manifest is not found.", gallery.displayName || gallery.name));
}
if (!isLanguagePackExtension(manifest) && !canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) {
const error = new Error(localize('cannot be installed', "Cannot install '{0}' extension since it cannot be enabled in the remote server.", gallery.displayName || gallery.name));
error.name = INSTALL_ERROR_NOT_SUPPORTED;
return Promise.reject(error);
}
return this.extensionManagementServerService.remoteExtensionManagementServer.extensionManagementService.installFromGallery(gallery);
}
return Promise.reject('No Servers to Install');

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert';
import * as sinon from 'sinon';
import { IExtensionManagementService, DidUninstallExtensionEvent, ILocalExtension, DidInstallExtensionEvent } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IExtensionEnablementService, EnablementState, IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { IExtensionEnablementService, EnablementState, IExtensionManagementServerService, IExtensionManagementServer } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { ExtensionEnablementService } from 'vs/workbench/services/extensionManagement/common/extensionEnablementService';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { Emitter } from 'vs/base/common/event';
@@ -39,13 +39,15 @@ function storageService(instantiationService: TestInstantiationService): IStorag
export class TestExtensionEnablementService extends ExtensionEnablementService {
constructor(instantiationService: TestInstantiationService) {
const extensionManagementService = instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService, { onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event, onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event } as IExtensionManagementService);
const extensionManagementServerService = instantiationService.get(IExtensionManagementServerService) || instantiationService.stub(IExtensionManagementServerService, <IExtensionManagementServerService>{ localExtensionManagementServer: { extensionManagementService } });
super(
storageService(instantiationService),
instantiationService.get(IWorkspaceContextService),
instantiationService.get(IWorkbenchEnvironmentService) || instantiationService.stub(IWorkbenchEnvironmentService, { configuration: Object.create(null) } as IWorkbenchEnvironmentService),
instantiationService.get(IExtensionManagementService) || instantiationService.stub(IExtensionManagementService,
{ onDidInstallExtension: new Emitter<DidInstallExtensionEvent>().event, onDidUninstallExtension: new Emitter<DidUninstallExtensionEvent>().event } as IExtensionManagementService),
instantiationService.get(IConfigurationService), instantiationService.get(IExtensionManagementServerService),
extensionManagementService,
instantiationService.get(IConfigurationService),
extensionManagementServerService,
productService
);
}
@@ -403,15 +405,23 @@ suite('ExtensionEnablementService Test', () => {
test('test local workspace extension is disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'workspace' }, { location: URI.file(`pub.a`) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(!testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test local workspace + ui extension is enabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace', 'ui'] }, { location: URI.file(`pub.a`) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
});
test('test local ui extension is not disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'ui' }, { location: URI.file(`pub.a`) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
@@ -419,29 +429,45 @@ suite('ExtensionEnablementService Test', () => {
test('test canChangeEnablement return false when the local workspace extension is disabled by kind', () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'workspace' }, { location: URI.file(`pub.a`) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), false);
});
test('test canChangeEnablement return true for local ui extension', () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'ui' }, { location: URI.file(`pub.a`) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true);
});
test('test remote ui extension is disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'ui' }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(!testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test remote ui+workspace extension is disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui', 'workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
});
test('test remote ui extension is disabled by kind when there is no local server', async () => {
instantiationService.stub(IExtensionManagementServerService, anExtensionManagementServerService(null, anExtensionManagementServer('vscode-remote', instantiationService)));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(!testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.DisabledByExtensionKind);
});
test('test remote workspace extension is not disabled by kind', async () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'workspace' }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.ok(testObject.isEnabled(localWorkspaceExtension));
assert.deepEqual(testObject.getEnablementState(localWorkspaceExtension), EnablementState.EnabledGlobally);
@@ -449,31 +475,35 @@ suite('ExtensionEnablementService Test', () => {
test('test canChangeEnablement return false when the remote ui extension is disabled by kind', () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'ui' }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['ui'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true);
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), false);
});
test('test canChangeEnablement return true for remote workspace extension', () => {
instantiationService.stub(IExtensionManagementServerService, aMultiExtensionManagementServerService(instantiationService));
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: 'workspace' }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
const localWorkspaceExtension = aLocalExtension2('pub.a', { extensionKind: ['workspace'] }, { location: URI.file(`pub.a`).with({ scheme: Schemas.vscodeRemote }) });
testObject = new TestExtensionEnablementService(instantiationService);
assert.equal(testObject.canChangeEnablement(localWorkspaceExtension), true);
});
});
function anExtensionManagementServer(authority: string, instantiationService: TestInstantiationService): IExtensionManagementServer {
return {
authority,
label: authority,
extensionManagementService: instantiationService.get(IExtensionManagementService)
};
}
function aMultiExtensionManagementServerService(instantiationService: TestInstantiationService): IExtensionManagementServerService {
const localExtensionManagementServer = {
authority: 'vscode-local',
label: 'local',
extensionManagementService: instantiationService.get(IExtensionManagementService)
};
const remoteExtensionManagementServer = {
authority: 'vscode-remote',
label: 'remote',
extensionManagementService: instantiationService.get(IExtensionManagementService)
};
const localExtensionManagementServer = anExtensionManagementServer('vscode-local', instantiationService);
const remoteExtensionManagementServer = anExtensionManagementServer('vscode-remote', instantiationService);
return anExtensionManagementServerService(localExtensionManagementServer, remoteExtensionManagementServer);
}
function anExtensionManagementServerService(localExtensionManagementServer: IExtensionManagementServer | null, remoteExtensionManagementServer: IExtensionManagementServer | null): IExtensionManagementServerService {
return {
_serviceBrand: undefined,
localExtensionManagementServer,

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
import * as dom from 'vs/base/browser/dom';
import { Schemas } from 'vs/base/common/network';
class ExtensionResourceLoaderService implements IExtensionResourceLoaderService {
_serviceBrand: undefined;
constructor(
@IFileService private readonly _fileService: IFileService
) { }
async readExtensionResource(uri: URI): Promise<string> {
uri = dom.asDomUri(uri);
if (uri.scheme !== Schemas.http && uri.scheme !== Schemas.https) {
const result = await this._fileService.readFile(uri);
return result.value.toString();
}
const response = await fetch(uri.toString(true));
if (response.status !== 200) {
throw new Error(response.statusText);
}
return response.text();
}
}
registerSingleton(IExtensionResourceLoaderService, ExtensionResourceLoaderService);

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* 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';
export const IExtensionResourceLoaderService = createDecorator<IExtensionResourceLoaderService>('extensionResourceLoaderService');
/**
* A service useful for reading resources from within extensions.
*/
export interface IExtensionResourceLoaderService {
_serviceBrand: undefined;
/**
* Read a certain resource within an extension.
*/
readExtensionResource(uri: URI): Promise<string>;
}

View File

@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
export class ExtensionResourceLoaderService implements IExtensionResourceLoaderService {
_serviceBrand: undefined;
constructor(
@IFileService private readonly _fileService: IFileService
) { }
async readExtensionResource(uri: URI): Promise<string> {
const result = await this._fileService.readFile(uri);
return result.value.toString();
}
}
registerSingleton(IExtensionResourceLoaderService, ExtensionResourceLoaderService);

View File

@@ -20,7 +20,7 @@ import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEn
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { WebWorkerExtensionHostStarter } from 'vs/workbench/services/extensions/browser/webWorkerExtensionHostStarter';
import { URI } from 'vs/base/common/uri';
import { isWebExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { canExecuteOnWeb } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { FetchFileSystemProvider } from 'vs/workbench/services/extensions/browser/webWorkerFileSystemProvider';
@@ -85,14 +85,14 @@ export class ExtensionService extends AbstractExtensionService implements IExten
protected _createExtensionHosts(_isInitialStart: boolean, initialActivationEvents: string[]): ExtensionHostProcessManager[] {
const result: ExtensionHostProcessManager[] = [];
const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => isWebExtension(ext, this._configService)));
const webExtensions = this.getExtensions().then(extensions => extensions.filter(ext => canExecuteOnWeb(ext, this._productService, this._configService)));
const webHostProcessWorker = this._instantiationService.createInstance(WebWorkerExtensionHostStarter, true, webExtensions, URI.file(this._environmentService.logsPath).with({ scheme: this._environmentService.logFile.scheme }));
const webHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, webHostProcessWorker, null, initialActivationEvents);
result.push(webHostProcessManager);
const remoteAgentConnection = this._remoteAgentService.getConnection();
if (remoteAgentConnection) {
const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !isWebExtension(ext, this._configService)));
const remoteExtensions = this.getExtensions().then(extensions => extensions.filter(ext => !canExecuteOnWeb(ext, this._productService, this._configService)));
const remoteExtHostProcessWorker = this._instantiationService.createInstance(RemoteExtensionHostClient, remoteExtensions, this._createProvider(remoteAgentConnection.remoteAuthority), this._remoteAgentService.socketFactory);
const remoteExtHostProcessManager = this._instantiationService.createInstance(ExtensionHostProcessManager, false, remoteExtHostProcessWorker, remoteAgentConnection.remoteAuthority, initialActivationEvents);
result.push(remoteExtHostProcessManager);
@@ -111,7 +111,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
let result: DeltaExtensionsResult;
// local: only enabled and web'ish extension
localExtensions = localExtensions.filter(ext => this._isEnabled(ext) && isWebExtension(ext, this._configService));
localExtensions = localExtensions!.filter(ext => this._isEnabled(ext) && canExecuteOnWeb(ext, this._productService, this._configService));
this._checkEnableProposedApi(localExtensions);
if (!remoteEnv) {
@@ -119,7 +119,7 @@ export class ExtensionService extends AbstractExtensionService implements IExten
} else {
// remote: only enabled and none-web'ish extension
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !isWebExtension(extension, this._configService));
remoteEnv.extensions = remoteEnv.extensions.filter(extension => this._isEnabled(extension) && !canExecuteOnWeb(extension, this._productService, this._configService));
this._checkEnableProposedApi(remoteEnv.extensions);
// in case of overlap, the remote wins

View File

@@ -428,4 +428,4 @@ export class ManageAuthorizedExtensionURIsAction extends Action {
}
const actionRegistry = Registry.as<IWorkbenchActionRegistry>(WorkbenchActionExtensions.WorkbenchActions);
actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ManageAuthorizedExtensionURIsAction, ManageAuthorizedExtensionURIsAction.ID, ManageAuthorizedExtensionURIsAction.LABEL), `Extensions: Manage Authorized Extension URIs...`, ExtensionsLabel);
actionRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ManageAuthorizedExtensionURIsAction, ManageAuthorizedExtensionURIsAction.ID, ManageAuthorizedExtensionURIsAction.LABEL), `Extensions: Manage Authorized Extension URIs...`, ExtensionsLabel);

View File

@@ -3,14 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IFileSystemProvider, FileSystemProviderCapabilities, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
import { FileSystemProviderCapabilities, IStat, FileType, FileDeleteOptions, FileOverwriteOptions, FileWriteOptions, FileSystemProviderError, FileSystemProviderErrorCode, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files';
import { Event } from 'vs/base/common/event';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { NotImplementedError } from 'vs/base/common/errors';
export class FetchFileSystemProvider implements IFileSystemProvider {
export class FetchFileSystemProvider implements IFileSystemProviderWithFileReadWriteCapability {
readonly capabilities = FileSystemProviderCapabilities.Readonly + FileSystemProviderCapabilities.FileReadWrite + FileSystemProviderCapabilities.PathCaseSensitive;
readonly onDidChangeCapabilities = Event.None;

View File

@@ -118,7 +118,12 @@ export class ExtensionDescriptionRegistry {
hasOnlyGoodArcs(id: string, good: Set<string>): boolean {
const dependencies = G.getArcs(id);
return dependencies.every(dependency => good.has(dependency));
for (let i = 0; i < dependencies.length; i++) {
if (!good.has(dependencies[i])) {
return false;
}
}
return true;
}
getNodes(): string[] {

View File

@@ -29,7 +29,7 @@ export function parseExtensionDevOptions(environmentService: IEnvironmentService
let isExtensionDevDebug = debugOk && typeof environmentService.debugExtensionHost.port === 'number';
let isExtensionDevDebugBrk = debugOk && !!environmentService.debugExtensionHost.break;
let isExtensionDevTestFromCli = isExtensionDevHost && !!environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.break;
let isExtensionDevTestFromCli = isExtensionDevHost && !!environmentService.extensionTestsLocationURI && !environmentService.debugExtensionHost.debugId;
return {
isExtensionDevHost,
isExtensionDevDebug,

View File

@@ -22,7 +22,7 @@ import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { Registry } from 'vs/platform/registry/common/platform';
import { Extensions as ActionExtensions, IWorkbenchActionRegistry } from 'vs/workbench/common/actions';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IUntitledResourceInput } from 'vs/workbench/common/editor';
import { IUntitledTextResourceInput } from 'vs/workbench/common/editor';
import { StopWatch } from 'vs/base/common/stopwatch';
import { VSBuffer } from 'vs/base/common/buffer';
import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions';
@@ -57,7 +57,7 @@ export class ExtensionHostProcessManager extends Disposable {
constructor(
public readonly isLocal: boolean,
extensionHostProcessWorker: IExtensionHostStarter,
private readonly _remoteAuthority: string,
private readonly _remoteAuthority: string | null,
initialActivationEvents: string[],
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@@ -185,7 +185,7 @@ export class ExtensionHostProcessManager extends Disposable {
this._extensionHostProcessRPCProtocol = new RPCProtocol(protocol, logger);
this._register(this._extensionHostProcessRPCProtocol.onDidChangeResponsiveState((responsiveState: ResponsiveState) => this._onDidChangeResponsiveState.fire(responsiveState)));
const extHostContext: IExtHostContext = {
remoteAuthority: this._remoteAuthority,
remoteAuthority: this._remoteAuthority! /* TODO: alexdima, remove not-null assertion */,
getProxy: <T>(identifier: ProxyIdentifier<T>): T => this._extensionHostProcessRPCProtocol!.getProxy(identifier),
set: <T, R extends T>(identifier: ProxyIdentifier<T>, instance: R): R => this._extensionHostProcessRPCProtocol!.set(identifier, instance),
assertRegistered: (identifiers: ProxyIdentifier<any>[]): void => this._extensionHostProcessRPCProtocol!.assertRegistered(identifiers),
@@ -362,7 +362,7 @@ class RPCLogger implements IRPCProtocolLogger {
}
interface ExtHostLatencyResult {
remoteAuthority: string;
remoteAuthority: string | null;
up: number;
down: number;
latency: number;
@@ -405,10 +405,13 @@ export class MeasureExtHostLatencyAction extends Action {
public async run(): Promise<any> {
const measurements = await Promise.all(getLatencyTestProviders().map(provider => provider.measure()));
this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledResourceInput);
this._editorService.openEditor({ contents: measurements.map(MeasureExtHostLatencyAction._print).join('\n\n'), options: { pinned: true } } as IUntitledTextResourceInput);
}
private static _print(m: ExtHostLatencyResult): string {
private static _print(m: ExtHostLatencyResult | null): string {
if (!m) {
return '';
}
return `${m.remoteAuthority ? `Authority: ${m.remoteAuthority}\n` : ``}Roundtrip latency: ${m.latency.toFixed(3)}ms\nUp: ${MeasureExtHostLatencyAction._printSpeed(m.up)}\nDown: ${MeasureExtHostLatencyAction._printSpeed(m.down)}\n`;
}
@@ -424,4 +427,4 @@ export class MeasureExtHostLatencyAction extends Action {
}
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency', nls.localize('developer', "Developer"));
registry.registerWorkbenchAction(SyncActionDescriptor.create(MeasureExtHostLatencyAction, MeasureExtHostLatencyAction.ID, MeasureExtHostLatencyAction.LABEL), 'Developer: Measure Extension Host Latency', nls.localize('developer', "Developer"));

View File

@@ -146,8 +146,20 @@ export class ExtensionPoint<T> implements IExtensionPoint<T> {
}
}
const extensionKindSchema: IJSONSchema = {
type: 'string',
enum: [
'ui',
'workspace'
],
enumDescriptions: [
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
],
};
const schemaId = 'vscode://schemas/vscode-extensions';
export const schema = {
export const schema: IJSONSchema = {
properties: {
engines: {
type: 'object',
@@ -345,17 +357,32 @@ export const schema = {
}
},
extensionKind: {
description: nls.localize('extensionKind', "Define the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions are run on the remote."),
type: 'string',
enum: [
'ui',
'workspace'
],
enumDescriptions: [
nls.localize('ui', "UI extension kind. In a remote window, such extensions are enabled only when available on the local machine."),
nls.localize('workspace', "Workspace extension kind. In a remote window, such extensions are enabled only when available on the remote.")
],
default: 'workspace'
description: nls.localize('extensionKind', "Define the kind of an extension. `ui` extensions are installed and run on the local machine while `workspace` extensions run on the remote."),
type: 'array',
items: extensionKindSchema,
default: ['workspace'],
defaultSnippets: [
{
body: ['ui'],
description: nls.localize('extensionKind.ui', "Define an extension which can run only on the local machine when connected to remote window.")
},
{
body: ['workspace'],
description: nls.localize('extensionKind.workspace', "Define an extension which can run only on the remote machine when connected remote window.")
},
{
body: ['ui', 'workspace'],
description: nls.localize('extensionKind.ui-workspace', "Define an extension which can run on either side, with a preference towards running on the local machine.")
},
{
body: ['workspace', 'ui'],
description: nls.localize('extensionKind.workspace-ui', "Define an extension which can run on either side, with a preference towards running on the remote machine.")
},
{
body: [],
description: nls.localize('extensionKind.empty', "Define an extension which cannot run in a remote context, neither on the local, nor on the remote machine.")
}
]
},
scripts: {
type: 'object',
@@ -395,7 +422,7 @@ export class ExtensionsRegistryImpl {
const result = new ExtensionPoint<T>(desc.extensionPoint, desc.defaultExtensionKind);
this._extensionPoints.set(desc.extensionPoint, result);
schema.properties['contributes'].properties[desc.extensionPoint] = desc.jsonSchema;
schema.properties!['contributes'].properties![desc.extensionPoint] = desc.jsonSchema;
schemaRegistry.registerSchema(schemaId, schema);
return result;

View File

@@ -4,55 +4,124 @@
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IExtensionManifest, ExtensionKind, ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { getGalleryExtensionId, areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { getGalleryExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { IProductService } from 'vs/platform/product/common/productService';
export function isWebExtension(manifest: IExtensionManifest, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, configurationService);
return extensionKind === 'web';
export function prefersExecuteOnUI(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, productService, configurationService);
return (extensionKind.length > 0 && extensionKind[0] === 'ui');
}
export function isUIExtension(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const uiContributions = ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').map(e => e.name);
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
const extensionKind = getExtensionKind(manifest, configurationService);
switch (extensionKind) {
case 'ui': return true;
case 'workspace': return false;
default: {
// Tagged as UI extension in product
if (isNonEmptyArray(productService.uiExtensions) && productService.uiExtensions.some(id => areSameExtensions({ id }, { id: extensionId }))) {
return true;
}
// Not an UI extension if it has main
if (manifest.main) {
return false;
}
// Not an UI extension if it has dependencies or an extension pack
if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) {
return false;
}
if (manifest.contributes) {
// Not an UI extension if it has no ui contributions
if (!uiContributions.length || Object.keys(manifest.contributes).some(contribution => uiContributions.indexOf(contribution) === -1)) {
return false;
}
}
return true;
}
}
export function prefersExecuteOnWorkspace(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, productService, configurationService);
return (extensionKind.length > 0 && extensionKind[0] === 'workspace');
}
function getExtensionKind(manifest: IExtensionManifest, configurationService: IConfigurationService): string | undefined {
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
const configuredExtensionKinds = configurationService.getValue<{ [key: string]: string }>('remote.extensionKind') || {};
for (const id of Object.keys(configuredExtensionKinds)) {
if (areSameExtensions({ id: extensionId }, { id })) {
return configuredExtensionKinds[id];
export function canExecuteOnUI(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, productService, configurationService);
return extensionKind.some(kind => kind === 'ui');
}
export function canExecuteOnWorkspace(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, productService, configurationService);
return extensionKind.some(kind => kind === 'workspace');
}
export function canExecuteOnWeb(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): boolean {
const extensionKind = getExtensionKind(manifest, productService, configurationService);
return extensionKind.some(kind => kind === 'web');
}
export function getExtensionKind(manifest: IExtensionManifest, productService: IProductService, configurationService: IConfigurationService): ExtensionKind[] {
// check in config
let result = getConfiguredExtensionKind(manifest, configurationService);
if (typeof result !== 'undefined') {
return toArray(result);
}
// check product.json
result = getProductExtensionKind(manifest, productService);
if (typeof result !== 'undefined') {
return toArray(result);
}
// check the manifest itself
result = manifest.extensionKind;
if (typeof result !== 'undefined') {
return toArray(result);
}
// Not an UI extension if it has main
if (manifest.main) {
return ['workspace'];
}
// Not an UI extension if it has dependencies or an extension pack
if (isNonEmptyArray(manifest.extensionDependencies) || isNonEmptyArray(manifest.extensionPack)) {
return ['workspace'];
}
if (manifest.contributes) {
// Not an UI extension if it has no ui contributions
for (const contribution of Object.keys(manifest.contributes)) {
if (!isUIExtensionPoint(contribution)) {
return ['workspace'];
}
}
}
return manifest.extensionKind;
return ['ui', 'workspace'];
}
let _uiExtensionPoints: Set<string> | null = null;
function isUIExtensionPoint(extensionPoint: string): boolean {
if (_uiExtensionPoints === null) {
const uiExtensionPoints = new Set<string>();
ExtensionsRegistry.getExtensionPoints().filter(e => e.defaultExtensionKind !== 'workspace').forEach(e => {
uiExtensionPoints.add(e.name);
});
_uiExtensionPoints = uiExtensionPoints;
}
return _uiExtensionPoints.has(extensionPoint);
}
let _productExtensionKindsMap: Map<string, ExtensionKind | ExtensionKind[]> | null = null;
function getProductExtensionKind(manifest: IExtensionManifest, productService: IProductService): ExtensionKind | ExtensionKind[] | undefined {
if (_productExtensionKindsMap === null) {
const productExtensionKindsMap = new Map<string, ExtensionKind | ExtensionKind[]>();
if (productService.extensionKind) {
for (const id of Object.keys(productService.extensionKind)) {
productExtensionKindsMap.set(ExtensionIdentifier.toKey(id), productService.extensionKind[id]);
}
}
_productExtensionKindsMap = productExtensionKindsMap;
}
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
return _productExtensionKindsMap.get(ExtensionIdentifier.toKey(extensionId));
}
let _configuredExtensionKindsMap: Map<string, ExtensionKind | ExtensionKind[]> | null = null;
function getConfiguredExtensionKind(manifest: IExtensionManifest, configurationService: IConfigurationService): ExtensionKind | ExtensionKind[] | undefined {
if (_configuredExtensionKindsMap === null) {
const configuredExtensionKindsMap = new Map<string, ExtensionKind | ExtensionKind[]>();
const configuredExtensionKinds = configurationService.getValue<{ [key: string]: ExtensionKind | ExtensionKind[] }>('remote.extensionKind') || {};
for (const id of Object.keys(configuredExtensionKinds)) {
configuredExtensionKindsMap.set(ExtensionIdentifier.toKey(id), configuredExtensionKinds[id]);
}
_configuredExtensionKindsMap = configuredExtensionKindsMap;
}
const extensionId = getGalleryExtensionId(manifest.publisher, manifest.name);
return _configuredExtensionKindsMap.get(ExtensionIdentifier.toKey(extensionId));
}
function toArray(extensionKind: ExtensionKind | ExtensionKind[]): ExtensionKind[] {
if (Array.isArray(extensionKind)) {
return extensionKind;
}
return extensionKind === 'ui' ? ['ui', 'workspace'] : [extensionKind];
}

View File

@@ -543,10 +543,16 @@ class MessageBuffer {
const el = arr[i];
const elType = arrType[i];
size += 1; // arg type
if (elType === ArgType.String) {
size += this.sizeLongString(el);
} else {
size += this.sizeVSBuffer(el);
switch (elType) {
case ArgType.String:
size += this.sizeLongString(el);
break;
case ArgType.VSBuffer:
size += this.sizeVSBuffer(el);
break;
case ArgType.Undefined:
// empty...
break;
}
}
return size;
@@ -557,19 +563,25 @@ class MessageBuffer {
for (let i = 0, len = arr.length; i < len; i++) {
const el = arr[i];
const elType = arrType[i];
if (elType === ArgType.String) {
this.writeUInt8(ArgType.String);
this.writeLongString(el);
} else {
this.writeUInt8(ArgType.VSBuffer);
this.writeVSBuffer(el);
switch (elType) {
case ArgType.String:
this.writeUInt8(ArgType.String);
this.writeLongString(el);
break;
case ArgType.VSBuffer:
this.writeUInt8(ArgType.VSBuffer);
this.writeVSBuffer(el);
break;
case ArgType.Undefined:
this.writeUInt8(ArgType.Undefined);
break;
}
}
}
public readMixedArray(): Array<string | VSBuffer> {
public readMixedArray(): Array<string | VSBuffer | undefined> {
const arrLen = this._buff.readUInt8(this._offset); this._offset += 1;
let arr: Array<string | VSBuffer> = new Array(arrLen);
let arr: Array<string | VSBuffer | undefined> = new Array(arrLen);
for (let i = 0; i < arrLen; i++) {
const argType = <ArgType>this.readUInt8();
switch (argType) {
@@ -579,6 +591,9 @@ class MessageBuffer {
case ArgType.VSBuffer:
arr[i] = this.readVSBuffer();
break;
case ArgType.Undefined:
arr[i] = undefined;
break;
}
}
return arr;
@@ -587,12 +602,20 @@ class MessageBuffer {
class MessageIO {
private static _arrayContainsBuffer(arr: any[]): boolean {
return arr.some(value => value instanceof VSBuffer);
private static _arrayContainsBufferOrUndefined(arr: any[]): boolean {
for (let i = 0, len = arr.length; i < len; i++) {
if (arr[i] instanceof VSBuffer) {
return true;
}
if (typeof arr[i] === 'undefined') {
return true;
}
}
return false;
}
public static serializeRequest(req: number, rpcId: number, method: string, args: any[], usesCancellationToken: boolean, replacer: JSONStringifyReplacer | null): VSBuffer {
if (this._arrayContainsBuffer(args)) {
if (this._arrayContainsBufferOrUndefined(args)) {
let massagedArgs: VSBuffer[] = [];
let massagedArgsType: ArgType[] = [];
for (let i = 0, len = args.length; i < len; i++) {
@@ -600,6 +623,9 @@ class MessageIO {
if (arg instanceof VSBuffer) {
massagedArgs[i] = arg;
massagedArgsType[i] = ArgType.VSBuffer;
} else if (typeof arg === 'undefined') {
massagedArgs[i] = VSBuffer.alloc(0);
massagedArgsType[i] = ArgType.Undefined;
} else {
massagedArgs[i] = VSBuffer.fromString(safeStringify(arg, replacer));
massagedArgsType[i] = ArgType.String;
@@ -767,5 +793,6 @@ const enum MessageType {
const enum ArgType {
String = 1,
VSBuffer = 2
VSBuffer = 2,
Undefined = 3
}

View File

@@ -37,7 +37,7 @@ import { parseExtensionDevOptions } from '../common/extensionDevOptions';
import { VSBuffer } from 'vs/base/common/buffer';
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
import { IExtensionHostStarter } from 'vs/workbench/services/extensions/common/extensions';
import { isEqualOrParent } from 'vs/base/common/resources';
import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { IHostService } from 'vs/workbench/services/host/browser/host';
export class ExtensionHostProcessWorker implements IExtensionHostStarter {
@@ -158,6 +158,8 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
'--nolazy',
(this._isExtensionDevDebugBrk ? '--inspect-brk=' : '--inspect=') + portNumber
];
} else {
opts.execArgv = ['--inspect-port=0'];
}
const crashReporterOptions = undefined; // TODO@electron pass this in as options to the extension host after verifying this actually works
@@ -170,10 +172,10 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
// Catch all output coming from the extension host process
type Output = { data: string, format: string[] };
this._extensionHostProcess.stdout.setEncoding('utf8');
this._extensionHostProcess.stderr.setEncoding('utf8');
const onStdout = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stdout, 'data');
const onStderr = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stderr, 'data');
this._extensionHostProcess.stdout!.setEncoding('utf8');
this._extensionHostProcess.stderr!.setEncoding('utf8');
const onStdout = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stdout!, 'data');
const onStderr = Event.fromNodeEventEmitter<string>(this._extensionHostProcess.stderr!, 'data');
const onOutput = Event.any(
Event.map(onStdout, o => ({ data: `%c${o}`, format: [''] })),
Event.map(onStderr, o => ({ data: `%c${o}`, format: ['color: red'] }))
@@ -411,7 +413,7 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
configuration: withNullAsUndefined(workspace.configuration),
id: workspace.id,
name: this._labelService.getWorkspaceLabel(workspace),
isUntitled: workspace.configuration ? isEqualOrParent(workspace.configuration, this._environmentService.untitledWorkspacesHome) : false
isUntitled: workspace.configuration ? isUntitledWorkspace(workspace.configuration, this._environmentService) : false
},
remote: {
authority: this._environmentService.configuration.remoteAuthority,
@@ -432,19 +434,19 @@ export class ExtensionHostProcessWorker implements IExtensionHostStarter {
private _logExtensionHostMessage(entry: IRemoteConsoleLog) {
// Send to local console unless we run tests from cli
if (!this._isExtensionDevTestFromCli) {
log(entry, 'Extension Host');
}
// Log on main side if running tests from cli
if (this._isExtensionDevTestFromCli) {
logRemoteEntry(this._logService, entry);
}
// Broadcast to other windows if we are in development mode
else if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) {
this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry);
// Log on main side if running tests from cli
logRemoteEntry(this._logService, entry);
} else {
// Send to local console
log(entry, 'Extension Host');
// Broadcast to other windows if we are in development mode
if (this._environmentService.debugExtensionHost.debugId && (!this._environmentService.isBuilt || this._isExtensionDevHost)) {
this._extensionHostDebugService.logToSession(this._environmentService.debugExtensionHost.debugId, entry);
}
}
}

View File

@@ -19,7 +19,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { IInitDataProvider, RemoteExtensionHostClient } from 'vs/workbench/services/extensions/common/remoteExtensionHostClient';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { IRemoteAuthorityResolverService, RemoteAuthorityResolverError, ResolverResult } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { isUIExtension as isUIExtensionFunc } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { getExtensionKind } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILifecycleService, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
@@ -439,8 +439,6 @@ export class ExtensionService extends AbstractExtensionService implements IExten
}
protected async _scanAndHandleExtensions(): Promise<void> {
const isUIExtension = (extension: IExtensionDescription) => isUIExtensionFunc(extension, this._productService, this._configurationService);
this._extensionScanner.startScanningExtensions(this.createLogger());
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
@@ -510,14 +508,38 @@ export class ExtensionService extends AbstractExtensionService implements IExten
// remove disabled extensions
remoteEnv.extensions = remove(remoteEnv.extensions, extension => this._isDisabled(extension));
// Determine where each extension will execute, based on extensionKind
const isInstalledLocally = new Set<string>();
localExtensions.forEach(ext => isInstalledLocally.add(ExtensionIdentifier.toKey(ext.identifier)));
const isInstalledRemotely = new Set<string>();
remoteEnv.extensions.forEach(ext => isInstalledRemotely.add(ExtensionIdentifier.toKey(ext.identifier)));
const enum RunningLocation { None, Local, Remote }
const pickRunningLocation = (extension: IExtensionDescription): RunningLocation => {
for (const extensionKind of getExtensionKind(extension, this._productService, this._configurationService)) {
if (extensionKind === 'ui') {
if (isInstalledLocally.has(ExtensionIdentifier.toKey(extension.identifier))) {
return RunningLocation.Local;
}
} else if (extensionKind === 'workspace') {
if (isInstalledRemotely.has(ExtensionIdentifier.toKey(extension.identifier))) {
return RunningLocation.Remote;
}
}
}
return RunningLocation.None;
};
const runningLocation = new Map<string, RunningLocation>();
localExtensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
remoteEnv.extensions.forEach(ext => runningLocation.set(ExtensionIdentifier.toKey(ext.identifier), pickRunningLocation(ext)));
// remove non-UI extensions from the local extensions
localExtensions = remove(localExtensions, extension => !extension.isBuiltin && !isUIExtension(extension));
localExtensions = localExtensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Local);
// in case of UI extensions overlap, the local extension wins
remoteEnv.extensions = remove(remoteEnv.extensions, localExtensions.filter(extension => isUIExtension(extension)));
// in case of other extensions overlap, the remote extension wins
localExtensions = remove(localExtensions, remoteEnv.extensions);
remoteEnv.extensions = remoteEnv.extensions.filter(ext => runningLocation.get(ExtensionIdentifier.toKey(ext.identifier)) === RunningLocation.Remote);
// save for remote extension's init data
this._remoteExtensionsEnvironmentData.set(remoteAuthority, remoteEnv);
@@ -550,14 +572,12 @@ export class ExtensionService extends AbstractExtensionService implements IExten
}
public _onExtensionHostExit(code: number): void {
// Expected development extension termination: When the extension host goes down we also shutdown the window
if (!this._isExtensionDevTestFromCli) {
this._electronService.closeWindow();
}
// When CLI testing make sure to exit with proper exit code
else {
if (this._isExtensionDevTestFromCli) {
// When CLI testing make sure to exit with proper exit code
ipc.send('vscode:exit', code);
} else {
// Expected development extension termination: When the extension host goes down we also shutdown the window
this._electronService.closeWindow();
}
}
}

View File

@@ -11,7 +11,7 @@ import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ILogService } from 'vs/platform/log/common/log';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { isUIExtension } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { prefersExecuteOnUI } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { isNonEmptyArray } from 'vs/base/common/arrays';
import { values } from 'vs/base/common/map';
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -48,6 +48,10 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC
}
private async doInstallFromGallery(extension: IGalleryExtension): Promise<ILocalExtension> {
if (this.configurationService.getValue<boolean>('remote.downloadExtensionsLocally')) {
this.logService.trace(`Download '${extension.identifier.id}' extension locally and install`);
return this.downloadCompatibleAndInstall(extension);
}
try {
const local = await super.installFromGallery(extension);
return local;
@@ -116,7 +120,7 @@ export class RemoteExtensionManagementChannelClient extends ExtensionManagementC
for (let idx = 0; idx < extensions.length; idx++) {
const extension = extensions[idx];
const manifest = manifests[idx];
if (manifest && isUIExtension(manifest, this.productService, this.configurationService) === uiExtension) {
if (manifest && prefersExecuteOnUI(manifest, this.productService, this.configurationService) === uiExtension) {
result.set(extension.identifier.id.toLowerCase(), extension);
extensionsManifests.push(manifest);
}

View File

@@ -27,6 +27,17 @@ interface ParsedExtHostArgs {
uriTransformerPath?: string;
}
// workaround for https://github.com/microsoft/vscode/issues/85490
// remove --inspect-port=0 after start so that it doesn't trigger LSP debugging
(function removeInspectPort() {
for (let i = 0; i < process.execArgv.length; i++) {
if (process.execArgv[i] === '--inspect-port=0') {
process.execArgv.splice(i, 1);
i--;
}
}
})();
const args = minimist(process.argv.slice(2), {
string: [
'uriTransformerPath'

View File

@@ -51,7 +51,7 @@ class ExtensionManifestParser extends ExtensionManifestHandler {
return pfs.readFile(this._absoluteManifestPath).then((manifestContents) => {
const errors: json.ParseError[] = [];
const manifest = json.parse(manifestContents.toString(), errors);
if (!!manifest && errors.length === 0) {
if (errors.length === 0 && json.getNodeType(manifest) === 'object') {
if (manifest.__metadata) {
manifest.uuid = manifest.__metadata.id;
}
@@ -108,6 +108,9 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
this._log.error(this._absoluteFolderPath, nls.localize('jsonsParseReportErrors', "Failed to parse {0}: {1}.", localized, getParseErrorMessage(error.error)));
});
};
const reportInvalidFormat = (localized: string | null): void => {
this._log.error(this._absoluteFolderPath, nls.localize('jsonInvalidFormat', "Invalid format {0}: JSON object expected.", localized));
};
let extension = path.extname(this._absoluteManifestPath);
let basename = this._absoluteManifestPath.substr(0, this._absoluteManifestPath.length - extension.length);
@@ -122,6 +125,9 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
if (errors.length > 0) {
reportErrors(translationPath, errors);
return { values: undefined, default: `${basename}.nls.json` };
} else if (json.getNodeType(translationBundle) !== 'object') {
reportInvalidFormat(translationPath);
return { values: undefined, default: `${basename}.nls.json` };
} else {
let values = translationBundle.contents ? translationBundle.contents.package : undefined;
return { values: values, default: `${basename}.nls.json` };
@@ -144,6 +150,9 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
if (errors.length > 0) {
reportErrors(messageBundle.localized, errors);
return { values: undefined, default: messageBundle.original };
} else if (json.getNodeType(messages) !== 'object') {
reportInvalidFormat(messageBundle.localized);
return { values: undefined, default: messageBundle.original };
}
return { values: messages, default: messageBundle.original };
}, (err) => {
@@ -165,6 +174,9 @@ class ExtensionManifestNLSReplacer extends ExtensionManifestHandler {
if (errors.length > 0) {
reportErrors(localizedMessages.default, errors);
return extensionDescription;
} else if (json.getNodeType(localizedMessages) !== 'object') {
reportInvalidFormat(localizedMessages.default);
return extensionDescription;
}
const localized = localizedMessages.values || Object.create(null);
ExtensionManifestNLSReplacer._replaceNLStrings(this._nlsConfig, extensionDescription, localized, defaults, this._log, this._absoluteFolderPath);
@@ -397,7 +409,15 @@ class ExtensionManifestValidator extends ExtensionManifestHandler {
}
private static _isStringArray(arr: string[]): boolean {
return Array.isArray(arr) && arr.every(value => typeof value === 'string');
if (!Array.isArray(arr)) {
return false;
}
for (let i = 0, len = arr.length; i < len; i++) {
if (typeof arr[i] !== 'string') {
return false;
}
}
return true;
}
}

View File

@@ -343,9 +343,13 @@ function patches(originals: typeof http | typeof https, resolveProxy: ReturnType
return original.apply(null, arguments as unknown as any[]);
}
const optionsPatched = options.agent instanceof ProxyAgent;
const originalAgent = options.agent;
if (originalAgent === true) {
throw new Error('Unexpected agent option: true');
}
const optionsPatched = originalAgent instanceof ProxyAgent;
const config = onRequest && ((<any>options)._vscodeProxySupport || /* LS */ (<any>options)._vscodeSystemProxy) || proxySetting.config;
const useProxySettings = !optionsPatched && (config === 'override' || config === 'on' && !options.agent);
const useProxySettings = !optionsPatched && (config === 'override' || config === 'on' && originalAgent === undefined);
const useSystemCertificates = !optionsPatched && certSetting.config && originals === https && !(options as https.RequestOptions).ca;
if (useProxySettings || useSystemCertificates) {
@@ -367,7 +371,7 @@ function patches(originals: typeof http | typeof https, resolveProxy: ReturnType
options.agent = new ProxyAgent({
resolveProxy: resolveProxy.bind(undefined, { useProxySettings, useSystemCertificates }),
defaultPort: originals === https ? 443 : 80,
originalAgent: options.agent
originalAgent
});
return original(options, callback);
}
@@ -469,7 +473,9 @@ async function readCaCertificates() {
}
async function readWindowsCaCertificates() {
const winCA = await import('vscode-windows-ca-certs');
const winCA = await new Promise<any>((resolve, reject) => {
require(['vscode-windows-ca-certs'], resolve, reject);
});
let ders: any[] = [];
const store = winCA();

View File

@@ -200,4 +200,15 @@ suite('RPCProtocol', () => {
done(null);
});
});
test('undefined arguments arrive as null', function () {
delegate = (a1: any, a2: any) => {
assert.equal(typeof a1, 'undefined');
assert.equal(a2, null);
return 7;
};
return bProxy.$m(undefined, null).then((res) => {
assert.equal(res, 7);
});
});
});

View File

@@ -12,8 +12,8 @@ import { IExtHostCommands, ExtHostCommands } from 'vs/workbench/api/common/extHo
import { IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IExtHostTerminalService, WorkerExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
// import { IExtHostTask, WorkerExtHostTask } from 'vs/workbench/api/common/extHostTask';
// import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
// import { IExtHostDebugService, WorkerExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch, ExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
@@ -32,6 +32,7 @@ registerSingleton(IExtHostCommands, ExtHostCommands);
registerSingleton(IExtHostDocumentsAndEditors, ExtHostDocumentsAndEditors);
registerSingleton(IExtHostStorage, ExtHostStorage);
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
registerSingleton(IExtHostSearch, ExtHostSearch);
// register services that only throw errors
function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
@@ -49,9 +50,8 @@ function NotImplementedProxy<T>(name: ServiceIdentifier<T>): { new(): T } {
};
}
registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService);
// registerSingleton(IExtHostTask, WorkerExtHostTask); {{SQL CARBON EDIT}} disable tasks
// registerSingleton(IExtHostDebugService, class extends NotImplementedProxy(IExtHostDebugService) { }); {{SQL CARBON EDIT}} remove debug service
registerSingleton(IExtHostSearch, class extends NotImplementedProxy(IExtHostSearch) { });
// registerSingleton(IExtHostTask, WorkerExtHostTask); {{SQL CARBON EDIT}} disable
// registerSingleton(IExtHostDebugService, WorkerExtHostDebugService); {{SQL CARBON EDIT}} disable
registerSingleton(IExtensionStoragePaths, class extends NotImplementedProxy(IExtensionStoragePaths) {
whenReady = Promise.resolve();
});

View File

@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* 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 { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService, ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration } from 'vs/platform/files/common/files';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { equals } from 'vs/base/common/objects';
export const AutoSaveAfterShortDelayContext = new RawContextKey<boolean>('autoSaveAfterShortDelayContext', false);
export interface IAutoSaveConfiguration {
autoSaveDelay?: number;
autoSaveFocusChange: boolean;
autoSaveApplicationChange: boolean;
}
export const enum AutoSaveMode {
OFF,
AFTER_SHORT_DELAY,
AFTER_LONG_DELAY,
ON_FOCUS_CHANGE,
ON_WINDOW_CHANGE
}
export const IFilesConfigurationService = createDecorator<IFilesConfigurationService>('filesConfigurationService');
export interface IFilesConfigurationService {
_serviceBrand: undefined;
//#region Auto Save
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration>;
getAutoSaveMode(): AutoSaveMode;
getAutoSaveConfiguration(): IAutoSaveConfiguration;
toggleAutoSave(): Promise<void>;
//#endregion
readonly onFilesAssociationChange: Event<void>;
readonly isHotExitEnabled: boolean;
readonly hotExitConfiguration: string | undefined;
}
export class FilesConfigurationService extends Disposable implements IFilesConfigurationService {
_serviceBrand: undefined;
private readonly _onAutoSaveConfigurationChange = this._register(new Emitter<IAutoSaveConfiguration>());
readonly onAutoSaveConfigurationChange = this._onAutoSaveConfigurationChange.event;
private readonly _onFilesAssociationChange = this._register(new Emitter<void>());
readonly onFilesAssociationChange = this._onFilesAssociationChange.event;
private configuredAutoSaveDelay?: number;
private configuredAutoSaveOnFocusChange: boolean | undefined;
private configuredAutoSaveOnWindowChange: boolean | undefined;
private autoSaveAfterShortDelayContext: IContextKey<boolean>;
private currentFilesAssociationConfig: { [key: string]: string; };
private currentHotExitConfig: string;
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService
) {
super();
this.autoSaveAfterShortDelayContext = AutoSaveAfterShortDelayContext.bindTo(contextKeyService);
const configuration = configurationService.getValue<IFilesConfiguration>();
this.currentFilesAssociationConfig = configuration?.files?.associations;
this.currentHotExitConfig = configuration?.files?.hotExit || HotExitConfiguration.ON_EXIT;
this.onFilesConfigurationChange(configuration);
this.registerListeners();
}
private registerListeners(): void {
// Files configuration changes
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files')) {
this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
}
}));
}
protected onFilesConfigurationChange(configuration: IFilesConfiguration): void {
// Auto Save
const autoSaveMode = configuration?.files?.autoSave || AutoSaveConfiguration.OFF;
switch (autoSaveMode) {
case AutoSaveConfiguration.AFTER_DELAY:
this.configuredAutoSaveDelay = configuration?.files?.autoSaveDelay;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = false;
break;
case AutoSaveConfiguration.ON_FOCUS_CHANGE:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = true;
this.configuredAutoSaveOnWindowChange = false;
break;
case AutoSaveConfiguration.ON_WINDOW_CHANGE:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = true;
break;
default:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = false;
break;
}
this.autoSaveAfterShortDelayContext.set(this.getAutoSaveMode() === AutoSaveMode.AFTER_SHORT_DELAY);
// Emit as event
this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
// Check for change in files associations
const filesAssociation = configuration?.files?.associations;
if (!equals(this.currentFilesAssociationConfig, filesAssociation)) {
this.currentFilesAssociationConfig = filesAssociation;
this._onFilesAssociationChange.fire();
}
// Hot exit
const hotExitMode = configuration?.files?.hotExit;
if (hotExitMode === HotExitConfiguration.OFF || hotExitMode === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
this.currentHotExitConfig = hotExitMode;
} else {
this.currentHotExitConfig = HotExitConfiguration.ON_EXIT;
}
}
getAutoSaveMode(): AutoSaveMode {
if (this.configuredAutoSaveOnFocusChange) {
return AutoSaveMode.ON_FOCUS_CHANGE;
}
if (this.configuredAutoSaveOnWindowChange) {
return AutoSaveMode.ON_WINDOW_CHANGE;
}
if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
}
return AutoSaveMode.OFF;
}
getAutoSaveConfiguration(): IAutoSaveConfiguration {
return {
autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : undefined,
autoSaveFocusChange: !!this.configuredAutoSaveOnFocusChange,
autoSaveApplicationChange: !!this.configuredAutoSaveOnWindowChange
};
}
async toggleAutoSave(): Promise<void> {
const setting = this.configurationService.inspect('files.autoSave');
let userAutoSaveConfig = setting.user;
if (isUndefinedOrNull(userAutoSaveConfig)) {
userAutoSaveConfig = setting.default; // use default if setting not defined
}
let newAutoSaveValue: string;
if ([AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, AutoSaveConfiguration.ON_WINDOW_CHANGE].some(s => s === userAutoSaveConfig)) {
newAutoSaveValue = AutoSaveConfiguration.OFF;
} else {
newAutoSaveValue = AutoSaveConfiguration.AFTER_DELAY;
}
return this.configurationService.updateValue('files.autoSave', newAutoSaveValue, ConfigurationTarget.USER);
}
get isHotExitEnabled(): boolean {
return !this.environmentService.isExtensionDevelopment && this.currentHotExitConfig !== HotExitConfiguration.OFF;
}
get hotExitConfiguration(): string {
return this.currentHotExitConfig;
}
}
registerSingleton(IFilesConfigurationService, FilesConfigurationService);

View File

@@ -33,6 +33,7 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { withNullAsUndefined } from 'vs/base/common/types';
import { addDisposableListener, EventType, EventHelper } from 'vs/base/browser/dom';
import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces';
import { Schemas } from 'vs/base/common/network';
/**
* Stores the selection & view state of an editor and allows to compare it to other selection states.
@@ -482,10 +483,10 @@ export class HistoryService extends Disposable implements IHistoryService {
}
private handleEditorEventInHistory(editor?: IBaseEditor): void {
const input = editor?.input;
// Ensure we have at least a name to show and not configured to exclude input
if (!input || !input.getName() || !this.include(input)) {
// Ensure we have not configured to exclude input and don't track invalid inputs
const input = editor?.input;
if (!input || input.isDisposed() || !this.include(input)) {
return;
}
@@ -592,10 +593,10 @@ export class HistoryService extends Disposable implements IHistoryService {
// stack but we need to keep our currentTextEditorState up to date with
// the navigtion that occurs.
if (this.navigatingInStack) {
if (codeEditor && control?.input) {
if (codeEditor && control?.input && !control.input.isDisposed()) {
this.currentTextEditorState = new TextEditorState(control.input, codeEditor.getSelection());
} else {
this.currentTextEditorState = null; // we navigated to a non text editor
this.currentTextEditorState = null; // we navigated to a non text or disposed editor
}
}
@@ -603,15 +604,15 @@ export class HistoryService extends Disposable implements IHistoryService {
else {
// navigation inside text editor
if (codeEditor && control?.input) {
if (codeEditor && control?.input && !control.input.isDisposed()) {
this.handleTextEditorEvent(control, codeEditor, event);
}
// navigation to non-text editor
// navigation to non-text disposed editor
else {
this.currentTextEditorState = null; // at this time we have no active text editor view state
if (control?.input) {
if (control?.input && !control.input.isDisposed()) {
this.handleNonTextEditorEvent(control);
}
}
@@ -735,8 +736,10 @@ export class HistoryService extends Disposable implements IHistoryService {
private preferResourceInput(input: IEditorInput): IEditorInput | IResourceInput {
const resource = input.getResource();
if (resource && this.fileService.canHandleResource(resource)) {
return { resource: resource };
if (resource && (resource.scheme === Schemas.file || resource.scheme === Schemas.vscodeRemote || resource.scheme === Schemas.userData)) {
// for now, only prefer well known schemes that we control to prevent
// issues such as https://github.com/microsoft/vscode/issues/85204
return { resource };
}
return input;

View File

@@ -146,7 +146,7 @@ export class BrowserHostService extends Disposable implements IHostService {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const openFolderInNewWindowConfig = windowConfig?.openFoldersInNewWindow || 'default' /* default */;
let openFolderInNewWindow = !!options.forceNewWindow && !options.forceReuseWindow;
let openFolderInNewWindow = (options.preferNewWindow || !!options.forceNewWindow) && !options.forceReuseWindow;
if (!options.forceNewWindow && !options.forceReuseWindow && (openFolderInNewWindowConfig === 'on' || openFolderInNewWindowConfig === 'off')) {
openFolderInNewWindow = (openFolderInNewWindowConfig === 'on');
}

View File

@@ -11,7 +11,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import { Keybinding, ResolvedKeybinding, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { KeybindingParser } from 'vs/base/common/keybindingParser';
import { OS, OperatingSystem, isWeb } from 'vs/base/common/platform';
import { OS, OperatingSystem } from 'vs/base/common/platform';
import { ICommandService, CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Extensions as ConfigExtensions, IConfigurationNode, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
@@ -19,7 +19,7 @@ import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/commo
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { AbstractKeybindingService } from 'vs/platform/keybinding/common/abstractKeybindingService';
import { IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource, IKeybindingService, IKeybindingEvent } from 'vs/platform/keybinding/common/keybinding';
import { IKeyboardEvent, IUserFriendlyKeybinding, KeybindingSource, IKeybindingService, IKeybindingEvent, KeybindingsSchemaContribution } from 'vs/platform/keybinding/common/keybinding';
import { KeybindingResolver } from 'vs/platform/keybinding/common/keybindingResolver';
import { IKeybindingItem, IKeybindingRule2, KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
@@ -43,8 +43,10 @@ import * as objects from 'vs/base/common/objects';
import { IKeymapService } from 'vs/workbench/services/keybinding/common/keymapInfo';
import { getDispatchConfig } from 'vs/workbench/services/keybinding/common/dispatchConfig';
import { isArray } from 'vs/base/common/types';
import { INavigatorWithKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard';
import { INavigatorWithKeyboard, IKeyboard } from 'vs/workbench/services/keybinding/browser/navigatorKeyboard';
import { ScanCodeUtils, IMMUTABLE_CODE_TO_KEY_CODE } from 'vs/base/common/scanCode';
import { flatten } from 'vs/base/common/arrays';
import { BrowserFeatures, KeyboardSupport } from 'vs/base/browser/canIUse';
interface ContributedKeyBinding {
command: string;
@@ -146,6 +148,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
private _keyboardMapper: IKeyboardMapper;
private _cachedResolver: KeybindingResolver | null;
private userKeybindings: UserKeybindings;
private readonly _contributions: KeybindingsSchemaContribution[] = [];
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@@ -161,7 +164,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
) {
super(contextKeyService, commandService, telemetryService, notificationService);
updateSchema();
this.updateSchema();
let dispatchConfig = getDispatchConfig(configurationService);
configurationService.onDidChangeConfiguration((e) => {
@@ -190,13 +193,6 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
});
this._register(this.userKeybindings.onDidChange(() => {
type CustomKeybindingsChangedClassification = {
keyCount: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true }
};
this._telemetryService.publicLog2<{ keyCount: number }, CustomKeybindingsChangedClassification>('customKeybindingsChanged', {
keyCount: this.userKeybindings.keybindings.length
});
this.updateResolver({
source: KeybindingSource.User,
keybindings: this.userKeybindings.keybindings
@@ -214,8 +210,8 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
this.updateResolver({ source: KeybindingSource.Default });
});
updateSchema();
this._register(extensionService.onDidRegisterExtensions(() => updateSchema()));
this.updateSchema();
this._register(extensionService.onDidRegisterExtensions(() => this.updateSchema()));
this._register(dom.addDisposableListener(window, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
let keyEvent = new StandardKeyboardEvent(e);
@@ -226,6 +222,28 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}));
let data = this.keymapService.getCurrentKeyboardLayout();
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"id": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"text": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"model" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"layout": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"variant": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"options": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"rules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"lang": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
/* __GDPR__
"keyboardLayout" : {
"currentKeyboardLayout": { "${inline}": [ "${IKeyboardLayoutInfo}" ] }
@@ -236,16 +254,16 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
});
this._register(browser.onDidChangeFullscreen(() => {
const keyboard = (<INavigatorWithKeyboard>navigator).keyboard;
const keyboard: IKeyboard | null = (<INavigatorWithKeyboard>navigator).keyboard;
if (!keyboard) {
if (BrowserFeatures.keyboard === KeyboardSupport.None) {
return;
}
if (browser.isFullscreen()) {
keyboard.lock(['Escape']);
keyboard?.lock(['Escape']);
} else {
keyboard.unlock();
keyboard?.unlock();
}
// update resolver which will bring back all unbound keyboard shortcuts
@@ -254,6 +272,18 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}));
}
public registerSchemaContribution(contribution: KeybindingsSchemaContribution): void {
this._contributions.push(contribution);
if (contribution.onDidChange) {
this._register(contribution.onDidChange(() => this.updateSchema()));
}
this.updateSchema();
}
private updateSchema() {
updateSchema(flatten(this._contributions.map(x => x.getSchemaAdditions())));
}
public _dumpDebugInfo(): string {
const layoutInfo = JSON.stringify(this.keymapService.getCurrentKeyboardLayout(), null, '\t');
const mapperInfo = this._keyboardMapper.dumpDebugInfo();
@@ -337,15 +367,11 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
}
private _assertBrowserConflicts(kb: Keybinding, commandId: string): boolean {
if (!isWeb) {
if (BrowserFeatures.keyboard === KeyboardSupport.Always) {
return false;
}
if (browser.isStandalone) {
return false;
}
if (browser.isFullscreen() && (<any>navigator).keyboard) {
if (BrowserFeatures.keyboard === KeyboardSupport.FullScreen && browser.isFullscreen()) {
return false;
}
@@ -503,7 +529,7 @@ export class WorkbenchKeybindingService extends AbstractKeybindingService {
);
}
private static _getDefaultKeybindings(defaultKeybindings: ResolvedKeybindingItem[]): string {
private static _getDefaultKeybindings(defaultKeybindings: readonly ResolvedKeybindingItem[]): string {
let out = new OutputBuilder();
out.writeLine('[');
@@ -653,7 +679,7 @@ let schema: IJSONSchema = {
let schemaRegistry = Registry.as<IJSONContributionRegistry>(Extensions.JSONContribution);
schemaRegistry.registerSchema(schemaId, schema);
function updateSchema() {
function updateSchema(additionalContributions: readonly IJSONSchema[]) {
commandsSchemas.length = 0;
commandsEnum.length = 0;
commandsEnumDescriptions.length = 0;
@@ -707,6 +733,9 @@ function updateSchema() {
for (const commandId of menuCommands.keys()) {
addKnownCommand(commandId);
}
commandsSchemas.push(...additionalContributions);
schemaRegistry.notifySchemaChanged(schemaId);
}
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigExtensions.Configuration);

View File

@@ -19,7 +19,7 @@ import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { URI } from 'vs/base/common/uri';
import { IFileService } from 'vs/platform/files/common/files';
import { RunOnceScheduler } from 'vs/base/common/async';
import { parse } from 'vs/base/common/json';
import { parse, getNodeType } from 'vs/base/common/json';
import * as objects from 'vs/base/common/objects';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { Registry } from 'vs/platform/registry/common/platform';
@@ -335,6 +335,10 @@ export class BrowserKeyboardMapperFactoryBase {
return true;
}
if (standardKeyboardEvent.browserEvent.key === 'Dead' || standardKeyboardEvent.browserEvent.isComposing) {
return true;
}
const mapping = currentKeymap.mapping[standardKeyboardEvent.code];
if (!mapping) {
@@ -482,9 +486,13 @@ class UserKeyboardLayout extends Disposable {
try {
const content = await this.fileService.readFile(this.keyboardLayoutResource);
const value = parse(content.value.toString());
const layoutInfo = value.layout;
const mappings = value.rawMapping;
this._keyboardLayout = KeymapInfo.createKeyboardLayoutFromDebugInfo(layoutInfo, mappings, true);
if (getNodeType(value) === 'object') {
const layoutInfo = value.layout;
const mappings = value.rawMapping;
this._keyboardLayout = KeymapInfo.createKeyboardLayoutFromDebugInfo(layoutInfo, mappings, true);
} else {
this._keyboardLayout = null;
}
} catch (e) {
this._keyboardLayout = null;
}

View File

@@ -250,7 +250,7 @@ export class KeybindingsEditingService extends Disposable implements IKeybinding
private parse(model: ITextModel): { result: IUserFriendlyKeybinding[], parseErrors: json.ParseError[] } {
const parseErrors: json.ParseError[] = [];
const result = json.parse(model.getValue(), parseErrors);
const result = json.parse(model.getValue(), parseErrors, { allowTrailingComma: true, allowEmptyContent: true });
return { result, parseErrors };
}

View File

@@ -38,19 +38,20 @@ import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editor
import { KeybindingsEditingService } from 'vs/workbench/services/keybinding/common/keybindingEditing';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { TextModelResolverService } from 'vs/workbench/services/textmodelResolver/common/textModelResolverService';
import { IUntitledEditorService, UntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestLifecycleService, TestTextFileService, TestTextResourcePropertiesService } from 'vs/workbench/test/workbenchTestServices';
import { IUntitledTextEditorService, UntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { TestBackupFileService, TestContextService, TestEditorGroupsService, TestEditorService, TestLifecycleService, TestTextFileService, TestTextResourcePropertiesService, TestWorkingCopyService } from 'vs/workbench/test/workbenchTestServices';
import { FileService } from 'vs/platform/files/common/fileService';
import { Schemas } from 'vs/base/common/network';
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
import { URI } from 'vs/base/common/uri';
import { FileUserDataProvider } from 'vs/workbench/services/userData/common/fileUserDataProvider';
import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { WorkbenchEnvironmentService } from 'vs/workbench/services/environment/node/environmentService';
import { NativeWorkbenchEnvironmentService } from 'vs/workbench/services/environment/electron-browser/environmentService';
import { IWindowConfiguration } from 'vs/platform/windows/common/windows';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
class TestEnvironmentService extends WorkbenchEnvironmentService {
class TestEnvironmentService extends NativeWorkbenchEnvironmentService {
constructor(private _appSettingsHome: URI) {
super(parseArgs(process.argv, OPTIONS) as IWindowConfiguration, process.execPath, 0);
@@ -67,7 +68,7 @@ interface Modifiers {
shiftKey?: boolean;
}
suite('KeybindingsEditing', () => {
suite.skip('KeybindingsEditing', () => {
let instantiationService: TestInstantiationService;
let testObject: KeybindingsEditingService;
@@ -93,6 +94,7 @@ suite('KeybindingsEditing', () => {
instantiationService.stub(IContextKeyService, <IContextKeyService>instantiationService.createInstance(MockContextKeyService));
instantiationService.stub(IEditorGroupsService, new TestEditorGroupsService());
instantiationService.stub(IEditorService, new TestEditorService());
instantiationService.stub(IWorkingCopyService, new TestWorkingCopyService());
instantiationService.stub(ITelemetryService, NullTelemetryService);
instantiationService.stub(IModeService, ModeServiceImpl);
instantiationService.stub(ILogService, new NullLogService());
@@ -103,7 +105,7 @@ suite('KeybindingsEditing', () => {
fileService.registerProvider(Schemas.file, diskFileSystemProvider);
fileService.registerProvider(Schemas.userData, new FileUserDataProvider(environmentService.appSettingsHome, environmentService.backupHome, diskFileSystemProvider, environmentService));
instantiationService.stub(IFileService, fileService);
instantiationService.stub(IUntitledEditorService, instantiationService.createInstance(UntitledEditorService));
instantiationService.stub(IUntitledTextEditorService, instantiationService.createInstance(UntitledTextEditorService));
instantiationService.stub(ITextFileService, instantiationService.createInstance(TestTextFileService));
instantiationService.stub(ITextModelService, <ITextModelService>instantiationService.createInstance(TextModelResolverService));
instantiationService.stub(IBackupFileService, new TestBackupFileService());

View File

@@ -12,10 +12,10 @@ import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry, IWo
import { Registry } from 'vs/platform/registry/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IWorkspaceContextService, IWorkspace } from 'vs/platform/workspace/common/workspace';
import { isEqual, basenameOrAuthority, isEqualOrParent, basename, joinPath, dirname } from 'vs/base/common/resources';
import { isEqual, basenameOrAuthority, basename, joinPath, dirname } from 'vs/base/common/resources';
import { tildify, getPathLabel } from 'vs/base/common/labels';
import { ltrim, endsWith } from 'vs/base/common/strings';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces';
import { IWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, isSingleFolderWorkspaceIdentifier, WORKSPACE_EXTENSION, toWorkspaceIdentifier, isWorkspaceIdentifier, isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { ILabelService, ResourceLabelFormatter, ResourceLabelFormatting } from 'vs/platform/label/common/label';
import { ExtensionsRegistry } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { match } from 'vs/base/common/glob';
@@ -117,7 +117,7 @@ export class LabelService implements ILabelService {
return;
}
if (match(formatter.authority, resource.authority) && (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length || ((formatter.authority.length === bestResult.authority.length) && formatter.priority))) {
if (match(formatter.authority.toLowerCase(), resource.authority.toLowerCase()) && (!bestResult || !bestResult.authority || formatter.authority.length > bestResult.authority.length || ((formatter.authority.length === bestResult.authority.length) && formatter.priority))) {
bestResult = formatter;
}
}
@@ -193,7 +193,7 @@ export class LabelService implements ILabelService {
if (isWorkspaceIdentifier(workspace)) {
// Workspace: Untitled
if (isEqualOrParent(workspace.configPath, this.environmentService.untitledWorkspacesHome)) {
if (isUntitledWorkspace(workspace.configPath, this.environmentService)) {
return localize('untitledWorkspace', "Untitled (Workspace)");
}

View File

@@ -41,6 +41,11 @@ export interface IWorkbenchLayoutService extends ILayoutService {
*/
readonly onFullscreenChange: Event<boolean>;
/**
* Emits when the window is maximized or unmaximized.
*/
readonly onMaximizeChange: Event<boolean>;
/**
* Emits when centered layout is enabled or disabled.
*/
@@ -114,6 +119,16 @@ export interface IWorkbenchLayoutService extends ILayoutService {
*/
toggleMaximizedPanel(): void;
/**
* Returns true if the window has a border.
*/
hasWindowBorder(): boolean;
/**
* Returns the window border radius if any.
*/
getWindowBorderRadius(): string | undefined;
/**
* Returns true if the panel is maximized.
*/
@@ -178,4 +193,15 @@ export interface IWorkbenchLayoutService extends ILayoutService {
* Register a part to participate in the layout.
*/
registerPart(part: Part): void;
/**
* Returns whether the window is maximized.
*/
isWindowMaximized(): boolean;
/**
* Updates the maximized state of the window.
*/
updateWindowMaximizedState(maximized: boolean): void;
}

View File

@@ -105,7 +105,7 @@ export class WorkbenchModeServiceImpl extends ModeServiceImpl {
this._configurationService = configurationService;
this._extensionService = extensionService;
languagesExtPoint.setHandler((extensions: IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => {
languagesExtPoint.setHandler((extensions: readonly IExtensionPointUser<IRawLanguageExtensionPoint[]>[]) => {
let allValidLanguages: ILanguageExtensionPoint[] = [];
for (let i = 0, len = extensions.length; i < len; i++) {

View File

@@ -21,7 +21,7 @@ export class PreferencesEditorInput extends SideBySideEditorInput {
return PreferencesEditorInput.ID;
}
getTitle(verbosity: Verbosity): string | undefined {
getTitle(verbosity: Verbosity): string {
return this.master.getTitle(verbosity);
}
}

View File

@@ -1117,7 +1117,7 @@ export function createValidator(prop: IConfigurationPropertySchema): (value: any
patternRegex = new RegExp(prop.pattern);
}
const type = Array.isArray(prop.type) ? prop.type : [prop.type];
const type: (string | undefined)[] = Array.isArray(prop.type) ? prop.type : [prop.type];
const canBeType = (t: string) => type.indexOf(t) > -1;
const isNullable = canBeType('null');

View File

@@ -577,7 +577,7 @@ suite('KeybindingsEditorModel test', () => {
function registerCommandWithTitle(command: string, title: string): void {
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(new SyncActionDescriptor(AnAction, command, title, { primary: 0 }), '');
registry.registerWorkbenchAction(SyncActionDescriptor.create(AnAction, command, title, { primary: 0 }), '');
}
function assertKeybindingItems(actual: ResolvedKeybindingItem[], expected: ResolvedKeybindingItem[]) {

View File

@@ -1,11 +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 { ProgressBarIndicator } from 'vs/workbench/services/progress/browser/progressIndicator';
export class EditorProgressService extends ProgressBarIndicator {
_serviceBrand: undefined;
}

View File

@@ -12,6 +12,7 @@
}
.monaco-workbench .progress-badge > .badge-content::before {
mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4=");
-webkit-mask: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNCIgaGVpZ2h0PSIxNCIgdmlld0JveD0iMiAyIDE0IDE0IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDIgMiAxNCAxNCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTkgMTZjLTMuODYgMC03LTMuMTQtNy03czMuMTQtNyA3LTdjMy44NTkgMCA3IDMuMTQxIDcgN3MtMy4xNDEgNy03IDd6bTAtMTIuNmMtMy4wODggMC01LjYgMi41MTMtNS42IDUuNnMyLjUxMiA1LjYgNS42IDUuNiA1LjYtMi41MTIgNS42LTUuNi0yLjUxMi01LjYtNS42LTUuNnptMy44NiA3LjFsLTMuMTYtMS44OTZ2LTMuODA0aC0xLjR2NC41OTZsMy44NCAyLjMwNS43Mi0xLjIwMXoiLz48L3N2Zz4=");
width: 14px;
height: 14px;

View File

@@ -8,11 +8,14 @@ import { isUndefinedOrNull } from 'vs/base/common/types';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IProgressRunner, IProgressIndicator } from 'vs/platform/progress/common/progress';
import { IProgressRunner, IProgressIndicator, emptyProgressRunner } from 'vs/platform/progress/common/progress';
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
export class ProgressBarIndicator implements IProgressIndicator {
export class ProgressBarIndicator extends Disposable implements IProgressIndicator {
constructor(private progressbar: ProgressBar) { }
constructor(protected progressbar: ProgressBar) {
super();
}
show(infinite: true, delay?: number): IProgressRunner;
show(total: number, delay?: number): IProgressRunner;
@@ -55,6 +58,55 @@ export class ProgressBarIndicator implements IProgressIndicator {
}
}
export class EditorProgressIndicator extends ProgressBarIndicator {
_serviceBrand: undefined;
constructor(progressBar: ProgressBar, private readonly group: IEditorGroupView) {
super(progressBar);
this.registerListeners();
}
private registerListeners() {
this._register(this.group.onDidCloseEditor(e => {
if (this.group.isEmpty) {
this.progressbar.stop().hide();
}
}));
}
show(infinite: true, delay?: number): IProgressRunner;
show(total: number, delay?: number): IProgressRunner;
show(infiniteOrTotal: true | number, delay?: number): IProgressRunner {
// No editor open: ignore any progress reporting
if (this.group.isEmpty) {
return emptyProgressRunner;
}
if (infiniteOrTotal === true) {
return super.show(true, delay);
}
return super.show(infiniteOrTotal, delay);
}
async showWhile(promise: Promise<any>, delay?: number): Promise<void> {
// No editor open: ignore any progress reporting
if (this.group.isEmpty) {
try {
await promise;
} catch (error) {
// ignore
}
}
return super.showWhile(promise, delay);
}
}
namespace ProgressIndicatorState {
export const enum Type {
@@ -65,9 +117,9 @@ namespace ProgressIndicatorState {
Work
}
export const None = new class { readonly type = Type.None; };
export const Done = new class { readonly type = Type.Done; };
export const Infinite = new class { readonly type = Type.Infinite; };
export const None = { type: Type.None } as const;
export const Done = { type: Type.Done } as const;
export const Infinite = { type: Type.Infinite } as const;
export class While {
readonly type = Type.While;

View File

@@ -45,7 +45,7 @@ export class ProgressService extends Disposable implements IProgressService {
super();
}
withProgress<R = unknown>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: (choice?: number) => void): Promise<R> {
async withProgress<R = unknown>(options: IProgressOptions, task: (progress: IProgress<IProgressStep>) => Promise<R>, onDidCancel?: (choice?: number) => void): Promise<R> {
const { location } = options;
if (typeof location === 'string') {
if (this.viewletService.getProgressIndicator(location)) {
@@ -56,7 +56,7 @@ export class ProgressService extends Disposable implements IProgressService {
return this.withPanelProgress(location, task, { ...options, location });
}
return Promise.reject(new Error(`Bad progress location: ${location}`));
throw new Error(`Bad progress location: ${location}`);
}
switch (location) {
@@ -73,7 +73,7 @@ export class ProgressService extends Disposable implements IProgressService {
case ProgressLocation.Dialog:
return this.withDialogProgress(options, task, onDidCancel);
default:
return Promise.reject(new Error(`Bad progress location: ${location}`));
throw new Error(`Bad progress location: ${location}`);
}
}

View File

@@ -11,11 +11,15 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { TestViewletService, TestPanelService } from 'vs/workbench/test/workbenchTestServices';
import { Event } from 'vs/base/common/event';
class TestViewlet implements IViewlet {
constructor(private id: string) { }
readonly onDidBlur = Event.None;
readonly onDidFocus = Event.None;
getId(): string { return this.id; }
getTitle(): string { return this.id; }
getActions(): IAction[] { return []; }

View File

@@ -0,0 +1,236 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionsRegistry, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ITunnelService } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEditableData } from 'vs/workbench/common/views';
export const IRemoteExplorerService = createDecorator<IRemoteExplorerService>('remoteExplorerService');
export const REMOTE_EXPLORER_TYPE_KEY: string = 'remote.explorerType';
export interface Tunnel {
remote: number;
localAddress: string;
local?: number;
name?: string;
description?: string;
closeable?: boolean;
}
export class TunnelModel extends Disposable {
readonly forwarded: Map<number, Tunnel>;
readonly published: Map<number, Tunnel>;
readonly candidates: Map<number, Tunnel>;
private _onForwardPort: Emitter<Tunnel> = new Emitter();
public onForwardPort: Event<Tunnel> = this._onForwardPort.event;
private _onClosePort: Emitter<number> = new Emitter();
public onClosePort: Event<number> = this._onClosePort.event;
private _onPortName: Emitter<number> = new Emitter();
public onPortName: Event<number> = this._onPortName.event;
constructor(
@ITunnelService private readonly tunnelService: ITunnelService
) {
super();
this.forwarded = new Map();
this.tunnelService.tunnels.then(tunnels => {
tunnels.forEach(tunnel => {
if (tunnel.localAddress) {
this.forwarded.set(tunnel.tunnelRemotePort, {
remote: tunnel.tunnelRemotePort,
localAddress: tunnel.localAddress,
local: tunnel.tunnelLocalPort
});
}
});
});
this.published = new Map();
this.candidates = new Map();
this._register(this.tunnelService.onTunnelOpened(tunnel => {
if (this.candidates.has(tunnel.tunnelRemotePort)) {
this.candidates.delete(tunnel.tunnelRemotePort);
}
if (!this.forwarded.has(tunnel.tunnelRemotePort) && tunnel.localAddress) {
this.forwarded.set(tunnel.tunnelRemotePort, {
remote: tunnel.tunnelRemotePort,
localAddress: tunnel.localAddress,
local: tunnel.tunnelLocalPort
});
}
this._onForwardPort.fire(this.forwarded.get(tunnel.tunnelRemotePort)!);
}));
this._register(this.tunnelService.onTunnelClosed(remotePort => {
if (this.forwarded.has(remotePort)) {
this.forwarded.delete(remotePort);
this._onClosePort.fire(remotePort);
}
}));
}
async forward(remote: number, local?: number, name?: string): Promise<void> {
if (!this.forwarded.has(remote)) {
const tunnel = await this.tunnelService.openTunnel(remote, local);
if (tunnel && tunnel.localAddress) {
const newForward: Tunnel = {
remote: tunnel.tunnelRemotePort,
local: tunnel.tunnelLocalPort,
name: name,
closeable: true,
localAddress: tunnel.localAddress
};
this.forwarded.set(remote, newForward);
this._onForwardPort.fire(newForward);
}
}
}
name(remote: number, name: string) {
if (this.forwarded.has(remote)) {
this.forwarded.get(remote)!.name = name;
this._onPortName.fire(remote);
}
}
async close(remote: number): Promise<void> {
return this.tunnelService.closeTunnel(remote);
}
address(remote: number): string | undefined {
return (this.forwarded.get(remote) || this.published.get(remote))?.localAddress;
}
}
export interface IRemoteExplorerService {
_serviceBrand: undefined;
onDidChangeTargetType: Event<string>;
targetType: string;
readonly helpInformation: HelpInformation[];
readonly tunnelModel: TunnelModel;
onDidChangeEditable: Event<number | undefined>;
setEditable(remote: number | undefined, data: IEditableData | null): void;
getEditableData(remote: number | undefined): IEditableData | undefined;
}
export interface HelpInformation {
extensionDescription: IExtensionDescription;
getStarted?: string;
documentation?: string;
feedback?: string;
issues?: string;
remoteName?: string[] | string;
}
const remoteHelpExtPoint = ExtensionsRegistry.registerExtensionPoint<HelpInformation>({
extensionPoint: 'remoteHelp',
jsonSchema: {
description: nls.localize('RemoteHelpInformationExtPoint', 'Contributes help information for Remote'),
type: 'object',
properties: {
'getStarted': {
description: nls.localize('RemoteHelpInformationExtPoint.getStarted', "The url to your project's Getting Started page"),
type: 'string'
},
'documentation': {
description: nls.localize('RemoteHelpInformationExtPoint.documentation', "The url to your project's documentation page"),
type: 'string'
},
'feedback': {
description: nls.localize('RemoteHelpInformationExtPoint.feedback', "The url to your project's feedback reporter"),
type: 'string'
},
'issues': {
description: nls.localize('RemoteHelpInformationExtPoint.issues', "The url to your project's issues list"),
type: 'string'
}
}
}
});
class RemoteExplorerService implements IRemoteExplorerService {
public _serviceBrand: undefined;
private _targetType: string = '';
private readonly _onDidChangeTargetType: Emitter<string> = new Emitter<string>();
public readonly onDidChangeTargetType: Event<string> = this._onDidChangeTargetType.event;
private _helpInformation: HelpInformation[] = [];
private _tunnelModel: TunnelModel;
private editable: { remote: number | undefined, data: IEditableData } | undefined;
private readonly _onDidChangeEditable: Emitter<number | undefined> = new Emitter();
public readonly onDidChangeEditable: Event<number | undefined> = this._onDidChangeEditable.event;
constructor(
@IStorageService private readonly storageService: IStorageService,
@ITunnelService tunnelService: ITunnelService) {
this._tunnelModel = new TunnelModel(tunnelService);
remoteHelpExtPoint.setHandler((extensions) => {
let helpInformation: HelpInformation[] = [];
for (let extension of extensions) {
this._handleRemoteInfoExtensionPoint(extension, helpInformation);
}
this._helpInformation = helpInformation;
});
}
set targetType(name: string) {
if (this._targetType !== name) {
this._targetType = name;
this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType, StorageScope.WORKSPACE);
this.storageService.store(REMOTE_EXPLORER_TYPE_KEY, this._targetType, StorageScope.GLOBAL);
this._onDidChangeTargetType.fire(this._targetType);
}
}
get targetType(): string {
return this._targetType;
}
private _handleRemoteInfoExtensionPoint(extension: IExtensionPointUser<HelpInformation>, helpInformation: HelpInformation[]) {
if (!extension.description.enableProposedApi) {
return;
}
if (!extension.value.documentation && !extension.value.feedback && !extension.value.getStarted && !extension.value.issues) {
return;
}
helpInformation.push({
extensionDescription: extension.description,
getStarted: extension.value.getStarted,
documentation: extension.value.documentation,
feedback: extension.value.feedback,
issues: extension.value.issues,
remoteName: extension.value.remoteName
});
}
get helpInformation(): HelpInformation[] {
return this._helpInformation;
}
get tunnelModel(): TunnelModel {
return this._tunnelModel;
}
setEditable(remote: number | undefined, data: IEditableData | null): void {
if (!data) {
this.editable = undefined;
} else {
this.editable = { remote, data };
}
this._onDidChangeEditable.fire(remote);
}
getEditableData(remote: number | undefined): IEditableData | undefined {
return this.editable && this.editable.remote === remote ? this.editable.data : undefined;
}
}
registerSingleton(IRemoteExplorerService, RemoteExplorerService, true);

View File

@@ -17,9 +17,10 @@ import { ISignService } from 'vs/platform/sign/common/sign';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { findFreePort } from 'vs/base/node/ports';
import { Event, Emitter } from 'vs/base/common/event';
export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemotePort: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, tunnelRemotePort);
export async function createRemoteTunnel(options: IConnectionOptions, tunnelRemotePort: number, tunnelLocalPort?: number): Promise<RemoteTunnel> {
const tunnel = new NodeRemoteTunnel(options, tunnelRemotePort, tunnelLocalPort);
return tunnel.waitForReady();
}
@@ -27,6 +28,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
public readonly tunnelRemotePort: number;
public tunnelLocalPort!: number;
public localAddress?: string;
private readonly _options: IConnectionOptions;
private readonly _server: net.Server;
@@ -35,7 +37,7 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
private readonly _listeningListener: () => void;
private readonly _connectionListener: (socket: net.Socket) => void;
constructor(options: IConnectionOptions, tunnelRemotePort: number) {
constructor(options: IConnectionOptions, tunnelRemotePort: number, private readonly suggestedLocalPort?: number) {
super();
this._options = options;
this._server = net.createServer();
@@ -61,12 +63,14 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
public async waitForReady(): Promise<this> {
// try to get the same port number as the remote port number...
const localPort = await findFreePort(this.tunnelRemotePort, 1, 1000);
const localPort = await findFreePort(this.suggestedLocalPort ?? this.tunnelRemotePort, 1, 1000);
// if that fails, the method above returns 0, which works out fine below...
this.tunnelLocalPort = (<net.AddressInfo>this._server.listen(localPort).address()).port;
const address = (<net.AddressInfo>this._server.listen(localPort).address());
this.tunnelLocalPort = address.port;
await this._barrier.wait();
this.localAddress = 'localhost:' + address.port;
return this;
}
@@ -96,6 +100,10 @@ class NodeRemoteTunnel extends Disposable implements RemoteTunnel {
export class TunnelService implements ITunnelService {
_serviceBrand: undefined;
private _onTunnelOpened: Emitter<RemoteTunnel> = new Emitter();
public onTunnelOpened: Event<RemoteTunnel> = this._onTunnelOpened.event;
private _onTunnelClosed: Emitter<number> = new Emitter();
public onTunnelClosed: Event<number> = this._onTunnelClosed.event;
private readonly _tunnels = new Map</* port */ number, { refcount: number, readonly value: Promise<RemoteTunnel> }>();
public constructor(
@@ -116,33 +124,51 @@ export class TunnelService implements ITunnelService {
this._tunnels.clear();
}
openTunnel(remotePort: number): Promise<RemoteTunnel> | undefined {
openTunnel(remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
if (!remoteAuthority) {
return undefined;
}
const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remotePort);
const resolvedTunnel = this.retainOrCreateTunnel(remoteAuthority, remotePort, localPort);
if (!resolvedTunnel) {
return resolvedTunnel;
}
return resolvedTunnel.then(tunnel => ({
return resolvedTunnel.then(tunnel => {
const newTunnel = this.makeTunnel(tunnel);
this._onTunnelOpened.fire(newTunnel);
return newTunnel;
});
}
private makeTunnel(tunnel: RemoteTunnel): RemoteTunnel {
return {
tunnelRemotePort: tunnel.tunnelRemotePort,
tunnelLocalPort: tunnel.tunnelLocalPort,
localAddress: tunnel.localAddress,
dispose: () => {
const existing = this._tunnels.get(remotePort);
const existing = this._tunnels.get(tunnel.tunnelRemotePort);
if (existing) {
if (--existing.refcount <= 0) {
existing.value.then(tunnel => tunnel.dispose());
this._tunnels.delete(remotePort);
this._tunnels.delete(tunnel.tunnelRemotePort);
this._onTunnelClosed.fire(tunnel.tunnelRemotePort);
}
}
}
}));
};
}
private retainOrCreateTunnel(remoteAuthority: string, remotePort: number): Promise<RemoteTunnel> | undefined {
async closeTunnel(remotePort: number): Promise<void> {
if (this._tunnels.has(remotePort)) {
const value = this._tunnels.get(remotePort)!;
(await value.value).dispose();
value.refcount = 0;
}
}
private retainOrCreateTunnel(remoteAuthority: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
const existing = this._tunnels.get(remotePort);
if (existing) {
++existing.refcount;
@@ -162,8 +188,9 @@ export class TunnelService implements ITunnelService {
logService: this.logService
};
const tunnel = createRemoteTunnel(options, remotePort);
this._tunnels.set(remotePort, { refcount: 1, value: tunnel });
const tunnel = createRemoteTunnel(options, remotePort, localPort);
// Using makeTunnel here for the value does result in dispose getting called twice, but it also ensures that _onTunnelClosed will be fired when closeTunnel is called.
this._tunnels.set(remotePort, { refcount: 1, value: tunnel.then(value => this.makeTunnel(value)) });
return tunnel;
}
}

View File

@@ -12,6 +12,7 @@ import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
import { nextTick } from 'vs/base/common/process';
export interface IInternalFileMatch {
base: URI;
@@ -114,7 +115,7 @@ class FileSearchEngine {
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
let providerSW: StopWatch;
new Promise(_resolve => process.nextTick(_resolve))
new Promise(_resolve => nextTick(_resolve))
.then(() => {
this.activeCancellationTokens.add(cancellation);

View File

@@ -61,9 +61,9 @@ export class ReplacePattern {
if (match) {
if (this.hasParameters) {
if (match[0] === text) {
return text.replace(this._regExp, this.pattern);
return text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
}
let replaceString = text.replace(this._regExp, this.pattern);
let replaceString = text.replace(this._regExp, this.buildReplaceString(match, preserveCase));
return replaceString.substr(match.index, match[0].length - (text.length - replaceString.length));
}
return this.buildReplaceString(match, preserveCase);

View File

@@ -329,6 +329,10 @@ export interface ISearchConfigurationProperties {
actionsPosition: 'auto' | 'right';
maintainFileSearchCache: boolean;
collapseResults: 'auto' | 'alwaysCollapse' | 'alwaysExpand';
searchOnType: boolean;
searchOnTypeDebouncePeriod: number;
enableSearchEditorPreview: boolean;
searchEditorPreviewForceAbsolutePaths: boolean;
}
export interface ISearchConfiguration extends IFilesConfiguration {

View File

@@ -19,7 +19,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { deserializeSearchError, FileMatch, ICachedSearchStats, IFileMatch, IFileQuery, IFileSearchStats, IFolderQuery, IProgressMessage, ISearchComplete, ISearchEngineStats, ISearchProgressItem, ISearchQuery, ISearchResultProvider, ISearchService, ITextQuery, pathIncludedInQuery, QueryType, SearchError, SearchErrorCode, SearchProviderType, isFileMatch, isProgressMessage } from 'vs/workbench/services/search/common/search';
import { addContextToEditorMatches, editorMatchesToTextSearchResults } from 'vs/workbench/services/search/common/searchHelpers';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class SearchService extends Disposable implements ISearchService {
@@ -32,7 +32,7 @@ export class SearchService extends Disposable implements ISearchService {
constructor(
private readonly modelService: IModelService,
private readonly untitledEditorService: IUntitledEditorService,
private readonly untitledTextEditorService: IUntitledTextEditorService,
private readonly editorService: IEditorService,
private readonly telemetryService: ITelemetryService,
private readonly logService: ILogService,
@@ -391,9 +391,15 @@ export class SearchService extends Disposable implements ISearchService {
return;
}
// Skip search results
if (model.getModeId() === 'search-result' && !(query.includePattern && query.includePattern['**/*.code-search'])) {
// TODO: untitled search editors will be excluded from search even when include *.code-search is specified
return;
}
// Support untitled files
if (resource.scheme === Schemas.untitled) {
if (!this.untitledEditorService.exists(resource)) {
if (!this.untitledTextEditorService.exists(resource)) {
return;
}
}
@@ -442,14 +448,14 @@ export class SearchService extends Disposable implements ISearchService {
export class RemoteSearchService extends SearchService {
constructor(
@IModelService modelService: IModelService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@IEditorService editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
@ILogService logService: ILogService,
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService
) {
super(modelService, untitledEditorService, editorService, telemetryService, logService, extensionService, fileService);
super(modelService, untitledTextEditorService, editorService, telemetryService, logService, extensionService, fileService);
}
}

View File

@@ -0,0 +1,355 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as resources from 'vs/base/common/resources';
import * as glob from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes';
import { nextTick } from 'vs/base/common/process';
export interface IFileUtils {
readdir: (resource: URI) => Promise<string[]>;
toCanonicalName: (encoding: string) => string;
}
export class TextSearchManager {
private collector: TextSearchResultsCollector | null = null;
private isLimitHit = false;
private resultCount = 0;
constructor(private query: ITextQuery, private provider: TextSearchProvider, private fileUtils: IFileUtils) { }
search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const folderQueries = this.query.folderQueries || [];
const tokenSource = new CancellationTokenSource();
token.onCancellationRequested(() => tokenSource.cancel());
return new Promise<ISearchCompleteStats>((resolve, reject) => {
this.collector = new TextSearchResultsCollector(onProgress);
let isCanceled = false;
const onResult = (result: TextSearchResult, folderIdx: number) => {
if (isCanceled) {
return;
}
if (!this.isLimitHit) {
const resultSize = this.resultSize(result);
if (extensionResultIsMatch(result) && typeof this.query.maxResults === 'number' && this.resultCount + resultSize > this.query.maxResults) {
this.isLimitHit = true;
isCanceled = true;
tokenSource.cancel();
result = this.trimResultToSize(result, this.query.maxResults - this.resultCount);
}
const newResultSize = this.resultSize(result);
this.resultCount += newResultSize;
if (newResultSize > 0) {
this.collector!.add(result, folderIdx);
}
}
};
// For each root folder
Promise.all(folderQueries.map((fq, i) => {
return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token);
})).then(results => {
tokenSource.dispose();
this.collector!.flush();
const someFolderHitLImit = results.some(result => !!result && !!result.limitHit);
resolve({
limitHit: this.isLimitHit || someFolderHitLImit,
stats: {
type: 'textSearchProvider'
}
});
}, (err: Error) => {
tokenSource.dispose();
const errMsg = toErrorMessage(err);
reject(new Error(errMsg));
});
});
}
private resultSize(result: TextSearchResult): number {
const match = <TextSearchMatch>result;
return Array.isArray(match.ranges) ?
match.ranges.length :
1;
}
private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch {
const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges];
const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches];
return {
ranges: rangesArr.slice(0, size),
preview: {
matches: matchesArr.slice(0, size),
text: result.preview.text
},
uri: result.uri
};
}
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise<TextSearchComplete | null | undefined> {
const queryTester = new QueryGlobTester(this.query, folderQuery);
const testingPs: Promise<void>[] = [];
const progress = {
report: (result: TextSearchResult) => {
if (!this.validateProviderResult(result)) {
return;
}
const hasSibling = folderQuery.folder.scheme === 'file' ?
glob.hasSiblingPromiseFn(() => {
return this.fileUtils.readdir(resources.dirname(result.uri));
}) :
undefined;
const relativePath = resources.relativePath(folderQuery.folder, result.uri);
if (relativePath) {
testingPs.push(
queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling)
.then(included => {
if (included) {
onResult(result);
}
}));
}
}
};
const searchOptions = this.getSearchOptionsForFolder(folderQuery);
return new Promise(resolve => nextTick(resolve))
.then(() => this.provider.provideTextSearchResults(patternInfoToQuery(this.query.contentPattern), searchOptions, progress, token))
.then(result => {
return Promise.all(testingPs)
.then(() => result);
});
}
private validateProviderResult(result: TextSearchResult): boolean {
if (extensionResultIsMatch(result)) {
if (Array.isArray(result.ranges)) {
if (!Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same type.');
return false;
}
if ((<Range[]>result.preview.matches).length !== result.ranges.length) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
} else {
if (Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
}
}
return true;
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): TextSearchOptions {
const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern);
const options = <TextSearchOptions>{
folder: URI.from(fq.folder),
excludes,
includes,
useIgnoreFiles: !fq.disregardIgnoreFiles,
useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles,
followSymlinks: !fq.ignoreSymlinks,
encoding: fq.fileEncoding && this.fileUtils.toCanonicalName(fq.fileEncoding),
maxFileSize: this.query.maxFileSize,
maxResults: this.query.maxResults,
previewOptions: this.query.previewOptions,
afterContext: this.query.afterContext,
beforeContext: this.query.beforeContext
};
(<IExtendedExtensionSearchOptions>options).usePCRE2 = this.query.usePCRE2;
return options;
}
}
function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery {
return <TextSearchQuery>{
isCaseSensitive: patternInfo.isCaseSensitive || false,
isRegExp: patternInfo.isRegExp || false,
isWordMatch: patternInfo.isWordMatch || false,
isMultiline: patternInfo.isMultiline || false,
pattern: patternInfo.pattern
};
}
export class TextSearchResultsCollector {
private _batchedCollector: BatchedCollector<IFileMatch>;
private _currentFolderIdx: number = -1;
private _currentUri: URI | undefined;
private _currentFileMatch: IFileMatch | null = null;
constructor(private _onResult: (result: IFileMatch[]) => void) {
this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
}
add(data: TextSearchResult, folderIdx: number): void {
// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
// providers that send results in random order. We could do this step afterwards instead.
if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || !resources.isEqual(this._currentUri, data.uri))) {
this.pushToCollector();
this._currentFileMatch = null;
}
if (!this._currentFileMatch) {
this._currentFolderIdx = folderIdx;
this._currentFileMatch = {
resource: data.uri,
results: []
};
}
this._currentFileMatch.results!.push(extensionResultToFrontendResult(data));
}
private pushToCollector(): void {
const size = this._currentFileMatch && this._currentFileMatch.results ?
this._currentFileMatch.results.length :
0;
this._batchedCollector.addItem(this._currentFileMatch!, size);
}
flush(): void {
this.pushToCollector();
this._batchedCollector.flush();
}
private sendItems(items: IFileMatch[]): void {
this._onResult(items);
}
}
function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult {
// Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these...
if (extensionResultIsMatch(data)) {
return <ITextSearchMatch>{
preview: {
matches: mapArrayOrNot(data.preview.matches, m => ({
startLineNumber: m.start.line,
startColumn: m.start.character,
endLineNumber: m.end.line,
endColumn: m.end.character
})),
text: data.preview.text
},
ranges: mapArrayOrNot(data.ranges, r => ({
startLineNumber: r.start.line,
startColumn: r.start.character,
endLineNumber: r.end.line,
endColumn: r.end.character
}))
};
} else {
return <ITextSearchContext>{
text: data.text,
lineNumber: data.lineNumber
};
}
}
export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch {
return !!(<TextSearchMatch>data).preview;
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
export class BatchedCollector<T> {
private static readonly TIMEOUT = 4000;
// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
private static readonly START_BATCH_AFTER_COUNT = 50;
private totalNumberCompleted = 0;
private batch: T[] = [];
private batchSize = 0;
private timeoutHandle: any;
constructor(private maxBatchSize: number, private cb: (items: T[]) => void) {
}
addItem(item: T, size: number): void {
if (!item) {
return;
}
this.addItemToBatch(item, size);
}
addItems(items: T[], size: number): void {
if (!items) {
return;
}
this.addItemsToBatch(items, size);
}
private addItemToBatch(item: T, size: number): void {
this.batch.push(item);
this.batchSize += size;
this.onUpdate();
}
private addItemsToBatch(item: T[], size: number): void {
this.batch = this.batch.concat(item);
this.batchSize += size;
this.onUpdate();
}
private onUpdate(): void {
if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
// Flush because we aren't batching yet
this.flush();
} else if (this.batchSize >= this.maxBatchSize) {
// Flush because the batch is full
this.flush();
} else if (!this.timeoutHandle) {
// No timeout running, start a timeout to flush
this.timeoutHandle = setTimeout(() => {
this.flush();
}, BatchedCollector.TIMEOUT);
}
}
flush(): void {
if (this.batchSize) {
this.totalNumberCompleted += this.batchSize;
this.cb(this.batch);
this.batch = [];
this.batchSize = 0;
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = 0;
}
}
}
}

View File

@@ -277,7 +277,7 @@ export class SearchService implements IRawSearchService {
for (const previousSearch in cache.resultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (strings.startsWith(searchValue, previousSearch)) {
if (hasPathSep && previousSearch.indexOf(sep) < 0) {
if (hasPathSep && previousSearch.indexOf(sep) < 0 && previousSearch !== '') {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
@@ -383,7 +383,7 @@ export class SearchService implements IRawSearchService {
cancel() {
// Do nothing
}
then(resolve: any, reject: any) {
then<TResult1 = C, TResult2 = never>(resolve?: ((value: C) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
return promise.then(resolve, reject);
}
catch(reject?: any) {

View File

@@ -79,15 +79,15 @@ export class RipgrepTextSearchEngine {
cancel();
});
rgProc.stdout.on('data', data => {
rgProc.stdout!.on('data', data => {
ripgrepParser.handleData(data);
});
let gotData = false;
rgProc.stdout.once('data', () => gotData = true);
rgProc.stdout!.once('data', () => gotData = true);
let stderr = '';
rgProc.stderr.on('data', data => {
rgProc.stderr!.on('data', data => {
const message = data.toString();
this.outputChannel.appendLine(message);
stderr += message;

View File

@@ -21,16 +21,17 @@ import { SearchChannelClient } from './searchIpc';
import { SearchService } from 'vs/workbench/services/search/common/searchService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { parseSearchPort } from 'vs/platform/environment/node/environmentService';
export class LocalSearchService extends SearchService {
constructor(
@IModelService modelService: IModelService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@IEditorService editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
@ILogService logService: ILogService,
@@ -39,10 +40,10 @@ export class LocalSearchService extends SearchService {
@IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService,
@IInstantiationService readonly instantiationService: IInstantiationService
) {
super(modelService, untitledEditorService, editorService, telemetryService, logService, extensionService, fileService);
super(modelService, untitledTextEditorService, editorService, telemetryService, logService, extensionService, fileService);
this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, environmentService.debugSearch);
this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, parseSearchPort(environmentService.args, environmentService.isBuilt));
}
}

View File

@@ -7,12 +7,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import * as pfs from 'vs/base/node/pfs';
import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchStats, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search';
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
export class TextSearchEngineAdapter {
constructor(private query: ITextQuery) {
}
constructor(private query: ITextQuery) { }
search(token: CancellationToken, onResult: (matches: ISerializedFileMatch[]) => void, onMessage: (message: IProgressMessage) => void): Promise<ISerializedSearchSuccess> {
if ((!this.query.folderQueries || !this.query.folderQueries.length) && (!this.query.extraFileResources || !this.query.extraFileResources.length)) {
@@ -30,7 +29,7 @@ export class TextSearchEngineAdapter {
onMessage({ message: msg });
}
};
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
return new Promise((resolve, reject) => {
return textSearchManager
.search(

View File

@@ -3,352 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as resources from 'vs/base/common/resources';
import * as glob from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { toCanonicalName } from 'vs/base/node/encoding';
import * as pfs from 'vs/base/node/pfs';
import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes';
import { ITextQuery } from 'vs/workbench/services/search/common/search';
import { TextSearchProvider } from 'vs/workbench/services/search/common/searchExtTypes';
import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager';
export class TextSearchManager {
export class NativeTextSearchManager extends TextSearchManager {
private collector: TextSearchResultsCollector | null = null;
private isLimitHit = false;
private resultCount = 0;
constructor(private query: ITextQuery, private provider: TextSearchProvider, private _pfs: typeof pfs = pfs) {
}
search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const folderQueries = this.query.folderQueries || [];
const tokenSource = new CancellationTokenSource();
token.onCancellationRequested(() => tokenSource.cancel());
return new Promise<ISearchCompleteStats>((resolve, reject) => {
this.collector = new TextSearchResultsCollector(onProgress);
let isCanceled = false;
const onResult = (result: TextSearchResult, folderIdx: number) => {
if (isCanceled) {
return;
}
if (!this.isLimitHit) {
const resultSize = this.resultSize(result);
if (extensionResultIsMatch(result) && typeof this.query.maxResults === 'number' && this.resultCount + resultSize > this.query.maxResults) {
this.isLimitHit = true;
isCanceled = true;
tokenSource.cancel();
result = this.trimResultToSize(result, this.query.maxResults - this.resultCount);
}
const newResultSize = this.resultSize(result);
this.resultCount += newResultSize;
if (newResultSize > 0) {
this.collector!.add(result, folderIdx);
}
}
};
// For each root folder
Promise.all(folderQueries.map((fq, i) => {
return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token);
})).then(results => {
tokenSource.dispose();
this.collector!.flush();
const someFolderHitLImit = results.some(result => !!result && !!result.limitHit);
resolve({
limitHit: this.isLimitHit || someFolderHitLImit,
stats: {
type: 'textSearchProvider'
}
});
}, (err: Error) => {
tokenSource.dispose();
const errMsg = toErrorMessage(err);
reject(new Error(errMsg));
});
constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs) {
super(query, provider, {
readdir: resource => _pfs.readdir(resource.fsPath),
toCanonicalName: name => toCanonicalName(name)
});
}
private resultSize(result: TextSearchResult): number {
const match = <TextSearchMatch>result;
return Array.isArray(match.ranges) ?
match.ranges.length :
1;
}
private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch {
const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges];
const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches];
return {
ranges: rangesArr.slice(0, size),
preview: {
matches: matchesArr.slice(0, size),
text: result.preview.text
},
uri: result.uri
};
}
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise<TextSearchComplete | null | undefined> {
const queryTester = new QueryGlobTester(this.query, folderQuery);
const testingPs: Promise<void>[] = [];
const progress = {
report: (result: TextSearchResult) => {
if (!this.validateProviderResult(result)) {
return;
}
const hasSibling = folderQuery.folder.scheme === 'file' ?
glob.hasSiblingPromiseFn(() => {
return this.readdir(path.dirname(result.uri.fsPath));
}) :
undefined;
const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath);
testingPs.push(
queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling)
.then(included => {
if (included) {
onResult(result);
}
}));
}
};
const searchOptions = this.getSearchOptionsForFolder(folderQuery);
return new Promise(resolve => process.nextTick(resolve))
.then(() => this.provider.provideTextSearchResults(patternInfoToQuery(this.query.contentPattern), searchOptions, progress, token))
.then(result => {
return Promise.all(testingPs)
.then(() => result);
});
}
private validateProviderResult(result: TextSearchResult): boolean {
if (extensionResultIsMatch(result)) {
if (Array.isArray(result.ranges)) {
if (!Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same type.');
return false;
}
if ((<Range[]>result.preview.matches).length !== result.ranges.length) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
} else {
if (Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
}
}
return true;
}
private readdir(dirname: string): Promise<string[]> {
return this._pfs.readdir(dirname);
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): TextSearchOptions {
const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern);
const options = <TextSearchOptions>{
folder: URI.from(fq.folder),
excludes,
includes,
useIgnoreFiles: !fq.disregardIgnoreFiles,
useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles,
followSymlinks: !fq.ignoreSymlinks,
encoding: fq.fileEncoding && toCanonicalName(fq.fileEncoding),
maxFileSize: this.query.maxFileSize,
maxResults: this.query.maxResults,
previewOptions: this.query.previewOptions,
afterContext: this.query.afterContext,
beforeContext: this.query.beforeContext
};
(<IExtendedExtensionSearchOptions>options).usePCRE2 = this.query.usePCRE2;
return options;
}
}
function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery {
return <TextSearchQuery>{
isCaseSensitive: patternInfo.isCaseSensitive || false,
isRegExp: patternInfo.isRegExp || false,
isWordMatch: patternInfo.isWordMatch || false,
isMultiline: patternInfo.isMultiline || false,
pattern: patternInfo.pattern
};
}
export class TextSearchResultsCollector {
private _batchedCollector: BatchedCollector<IFileMatch>;
private _currentFolderIdx: number = -1;
private _currentUri: URI | undefined;
private _currentFileMatch: IFileMatch | null = null;
constructor(private _onResult: (result: IFileMatch[]) => void) {
this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
}
add(data: TextSearchResult, folderIdx: number): void {
// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
// providers that send results in random order. We could do this step afterwards instead.
if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || !resources.isEqual(this._currentUri, data.uri))) {
this.pushToCollector();
this._currentFileMatch = null;
}
if (!this._currentFileMatch) {
this._currentFolderIdx = folderIdx;
this._currentFileMatch = {
resource: data.uri,
results: []
};
}
this._currentFileMatch.results!.push(extensionResultToFrontendResult(data));
}
private pushToCollector(): void {
const size = this._currentFileMatch && this._currentFileMatch.results ?
this._currentFileMatch.results.length :
0;
this._batchedCollector.addItem(this._currentFileMatch!, size);
}
flush(): void {
this.pushToCollector();
this._batchedCollector.flush();
}
private sendItems(items: IFileMatch[]): void {
this._onResult(items);
}
}
function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult {
// Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these...
if (extensionResultIsMatch(data)) {
return <ITextSearchMatch>{
preview: {
matches: mapArrayOrNot(data.preview.matches, m => ({
startLineNumber: m.start.line,
startColumn: m.start.character,
endLineNumber: m.end.line,
endColumn: m.end.character
})),
text: data.preview.text
},
ranges: mapArrayOrNot(data.ranges, r => ({
startLineNumber: r.start.line,
startColumn: r.start.character,
endLineNumber: r.end.line,
endColumn: r.end.character
}))
};
} else {
return <ITextSearchContext>{
text: data.text,
lineNumber: data.lineNumber
};
}
}
export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch {
return !!(<TextSearchMatch>data).preview;
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
export class BatchedCollector<T> {
private static readonly TIMEOUT = 4000;
// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
private static readonly START_BATCH_AFTER_COUNT = 50;
private totalNumberCompleted = 0;
private batch: T[] = [];
private batchSize = 0;
private timeoutHandle: any;
constructor(private maxBatchSize: number, private cb: (items: T[]) => void) {
}
addItem(item: T, size: number): void {
if (!item) {
return;
}
this.addItemToBatch(item, size);
}
addItems(items: T[], size: number): void {
if (!items) {
return;
}
this.addItemsToBatch(items, size);
}
private addItemToBatch(item: T, size: number): void {
this.batch.push(item);
this.batchSize += size;
this.onUpdate();
}
private addItemsToBatch(item: T[], size: number): void {
this.batch = this.batch.concat(item);
this.batchSize += size;
this.onUpdate();
}
private onUpdate(): void {
if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
// Flush because we aren't batching yet
this.flush();
} else if (this.batchSize >= this.maxBatchSize) {
// Flush because the batch is full
this.flush();
} else if (!this.timeoutHandle) {
// No timeout running, start a timeout to flush
this.timeoutHandle = setTimeout(() => {
this.flush();
}, BatchedCollector.TIMEOUT);
}
}
flush(): void {
if (this.batchSize) {
this.totalNumberCompleted += this.batchSize;
this.cb(this.batch);
this.batch = [];
this.batchSize = 0;
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = 0;
}
}
}
}

View File

@@ -214,5 +214,21 @@ suite('Replace Pattern test', () => {
testObject = new ReplacePattern('$0ah', { pattern: 'b(la)(?=\\stext$)', isRegExp: true });
actual = testObject.getReplaceString('this is a bla text');
assert.equal('blaah', actual);
testObject = new ReplacePattern('newrege$1', true, /Testrege(\w*)/);
actual = testObject.getReplaceString('Testregex', true);
assert.equal('Newregex', actual);
testObject = new ReplacePattern('newrege$1', true, /TESTREGE(\w*)/);
actual = testObject.getReplaceString('TESTREGEX', true);
assert.equal('NEWREGEX', actual);
testObject = new ReplacePattern('new_rege$1', true, /Test_Rege(\w*)/);
actual = testObject.getReplaceString('Test_Regex', true);
assert.equal('New_Regex', actual);
testObject = new ReplacePattern('new-rege$1', true, /Test-Rege(\w*)/);
actual = testObject.getReplaceString('Test-Regex', true);
assert.equal('New-Regex', actual);
});
});
});

View File

@@ -9,9 +9,9 @@ import { URI } from 'vs/base/common/uri';
import { Progress } from 'vs/platform/progress/common/progress';
import { ITextQuery, QueryType } from 'vs/workbench/services/search/common/search';
import { ProviderResult, TextSearchComplete, TextSearchOptions, TextSearchProvider, TextSearchQuery, TextSearchResult } from 'vs/workbench/services/search/common/searchExtTypes';
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
suite('TextSearchManager', () => {
suite('NativeTextSearchManager', () => {
test('fixes encoding', async () => {
let correctEncoding = false;
const provider: TextSearchProvider = {
@@ -33,7 +33,7 @@ suite('TextSearchManager', () => {
}]
};
const m = new TextSearchManager(query, provider);
const m = new NativeTextSearchManager(query, provider);
await m.search(() => { }, new CancellationTokenSource().token);
assert.ok(correctEncoding);

View File

@@ -6,6 +6,7 @@
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable } from 'vs/base/common/lifecycle';
import { ThemeColor } from 'vs/platform/theme/common/themeService';
import { Event } from 'vs/base/common/event';
export const IStatusbarService = createDecorator<IStatusbarService>('statusbarService');
@@ -74,7 +75,17 @@ export interface IStatusbarService {
addEntry(entry: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority?: number): IStatusbarEntryAccessor;
/**
* Allows to update an entry's visibilty with the provided ID.
* An event that is triggered when an entry's visibility is changed.
*/
readonly onDidChangeEntryVisibility: Event<{ id: string, visible: boolean }>;
/**
* Return if an entry is visible or not.
*/
isEntryVisible(id: string): boolean;
/**
* Allows to update an entry's visibility with the provided ID.
*/
updateEntryVisibility(id: string, visible: boolean): void;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { ITelemetryService, ITelemetryInfo, ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService, combinedAppender, LogAppender, ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils';
import { NullTelemetryService, combinedAppender, LogAppender, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
@@ -19,18 +19,11 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
export class WebTelemetryAppender implements ITelemetryAppender {
constructor(private _logService: ILogService, private _appender: IRemoteAgentService,
@IWorkbenchEnvironmentService private _environmentService: IWorkbenchEnvironmentService) { } // {{ SQL CARBON EDIT }}
constructor(private _logService: ILogService, private _appender: IRemoteAgentService) { }
log(eventName: string, data: any): void {
data = validateTelemetryData(data);
this._logService.trace(`telemetry/${eventName}`, data);
const eventPrefix = this._environmentService.appQuality !== 'stable' ? '/adsworkbench/' : '/monacoworkbench/'; // {{SQL CARBON EDIT}}
this._appender.logTelemetry(eventPrefix + eventName, {
properties: data.properties,
measurements: data.measurements
});
this._appender.logTelemetry(eventName, data);
}
flush(): Promise<void> {
@@ -54,11 +47,10 @@ export class TelemetryService extends Disposable implements ITelemetryService {
) {
super();
if (!environmentService.args['disable-telemetry'] && !!productService.enableTelemetry) {
if (!!productService.enableTelemetry) {
const config: ITelemetryServiceConfig = {
appender: combinedAppender(new WebTelemetryAppender(logService, remoteAgentService, environmentService), new LogAppender(logService)), // {{SQL CARBON EDIT}}
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, environmentService.configuration.remoteAuthority),
piiPaths: [environmentService.appRoot]
appender: combinedAppender(new WebTelemetryAppender(logService, remoteAgentService), new LogAppender(logService)),
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.remoteAuthority, environmentService.options && environmentService.options.resolveCommonTelemetryProperties)
};
this.impl = this._register(new BaseTelemetryService(config, configurationService));

View File

@@ -38,7 +38,7 @@ export class TelemetryService extends Disposable implements ITelemetryService {
const channel = sharedProcessService.getChannel('telemetryAppender');
const config: ITelemetryServiceConfig = {
appender: combinedAppender(new TelemetryAppenderClient(channel), new LogAppender(logService)),
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority),
commonProperties: resolveWorkbenchCommonProperties(storageService, productService.commit, productService.version, environmentService.configuration.machineId!, productService.msftInternalDomains, environmentService.installSourcePath, environmentService.configuration.remoteAuthority),
piiPaths: environmentService.extensionsPath ? [environmentService.appRoot, environmentService.extensionsPath] : [environmentService.appRoot]
};

View File

@@ -10,25 +10,26 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import * as resources from 'vs/base/common/resources';
import * as types from 'vs/base/common/types';
import { equals as equalArray } from 'vs/base/common/arrays';
import { URI } from 'vs/base/common/uri';
import { TokenizationResult, TokenizationResult2 } from 'vs/editor/common/core/token';
import { IState, ITokenizationSupport, LanguageId, TokenMetadata, TokenizationRegistry, StandardTokenType, LanguageIdentifier } from 'vs/editor/common/modes';
import { nullTokenize2 } from 'vs/editor/common/modes/nullMode';
import { generateTokensCSSForColorMap } from 'vs/editor/common/modes/supports/tokenization';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ExtensionMessageCollector } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ITMSyntaxExtensionPoint, grammarsExtPoint } from 'vs/workbench/services/textMate/common/TMGrammars';
import { ITextMateService } from 'vs/workbench/services/textMate/common/textMateService';
import { ITokenColorizationRule, IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { ITextMateThemingRule, IWorkbenchThemeService, IColorTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IGrammar, StackElement, IOnigLib, IRawTheme } from 'vscode-textmate';
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IValidGrammarDefinition, IValidEmbeddedLanguagesMap, IValidTokenTypeMap } from 'vs/workbench/services/textMate/common/TMScopeRegistry';
import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
export abstract class AbstractTextMateService extends Disposable implements ITextMateService {
public _serviceBrand: undefined;
@@ -44,11 +45,12 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
private _grammarFactory: TMGrammarFactory | null;
private _tokenizersRegistrations: IDisposable[];
protected _currentTheme: IRawTheme | null;
protected _currentTokenColorMap: string[] | null;
constructor(
@IModeService private readonly _modeService: IModeService,
@IWorkbenchThemeService private readonly _themeService: IWorkbenchThemeService,
@IFileService protected readonly _fileService: IFileService,
@IExtensionResourceLoaderService protected readonly _extensionResourceLoaderService: IExtensionResourceLoaderService,
@INotificationService private readonly _notificationService: INotificationService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -65,6 +67,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
this._tokenizersRegistrations = [];
this._currentTheme = null;
this._currentTokenColorMap = null;
grammarsExtPoint.setHandler((extensions) => {
this._grammarDefinitions = null;
@@ -191,10 +194,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
this._grammarFactory = new TMGrammarFactory({
logTrace: (msg: string) => this._logService.trace(msg),
logError: (msg: string, err: any) => this._logService.error(msg, err),
readFile: async (resource: URI) => {
const content = await this._fileService.readFile(resource);
return content.value.toString();
}
readFile: (resource: URI) => this._extensionResourceLoaderService.readExtensionResource(resource)
}, this._grammarDefinitions || [], vscodeTextmate, this._loadOnigLib());
this._onDidCreateGrammarFactory(this._grammarDefinitions || []);
@@ -221,6 +221,9 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
return null;
}
const r = await grammarFactory.createGrammar(languageId);
if (!r.grammar) {
return null;
}
const tokenization = new TMTokenization(r.grammar, r.initialState, r.containsEmbeddedLanguages);
tokenization.onDidEncounterLanguage((languageId) => {
if (!this._encounteredLanguages[languageId]) {
@@ -245,22 +248,23 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
}
private _updateTheme(grammarFactory: TMGrammarFactory, colorTheme: IColorTheme, forceUpdate: boolean): void {
if (!forceUpdate && this._currentTheme && AbstractTextMateService.equalsTokenRules(this._currentTheme.settings, colorTheme.tokenColors)) {
if (!forceUpdate && this._currentTheme && this._currentTokenColorMap && AbstractTextMateService.equalsTokenRules(this._currentTheme.settings, colorTheme.tokenColors) && equalArray(this._currentTokenColorMap, colorTheme.tokenColorMap)) {
return;
}
this._currentTheme = { name: colorTheme.label, settings: colorTheme.tokenColors };
this._doUpdateTheme(grammarFactory, this._currentTheme);
this._currentTokenColorMap = colorTheme.tokenColorMap;
this._doUpdateTheme(grammarFactory, this._currentTheme, this._currentTokenColorMap);
}
protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme): void {
grammarFactory.setTheme(theme);
let colorMap = AbstractTextMateService._toColorMap(grammarFactory.getColorMap());
protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme, tokenColorMap: string[]): void {
grammarFactory.setTheme(theme, tokenColorMap);
let colorMap = AbstractTextMateService._toColorMap(tokenColorMap);
let cssRules = generateTokensCSSForColorMap(colorMap);
this._styleElement.innerHTML = cssRules;
TokenizationRegistry.setColorMap(colorMap);
}
private static equalsTokenRules(a: ITokenColorizationRule[] | null, b: ITokenColorizationRule[] | null): boolean {
private static equalsTokenRules(a: ITextMateThemingRule[] | null, b: ITextMateThemingRule[] | null): boolean {
if (!b || !a || b.length !== a.length) {
return false;
}
@@ -317,7 +321,7 @@ export abstract class AbstractTextMateService extends Disposable implements ITex
return true;
}
public async createGrammar(modeId: string): Promise<IGrammar> {
public async createGrammar(modeId: string): Promise<IGrammar | null> {
const grammarFactory = await this._getOrCreateGrammarFactory();
const { grammar } = await grammarFactory.createGrammar(this._modeService.getLanguageIdentifier(modeId)!.id);
return grammar;

View File

@@ -8,25 +8,25 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { AbstractTextMateService } from 'vs/workbench/services/textMate/browser/abstractTextMateService';
import { IOnigLib } from 'vscode-textmate';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
export class TextMateService extends AbstractTextMateService {
constructor(
@IModeService modeService: IModeService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IFileService fileService: IFileService,
@IExtensionResourceLoaderService extensionResourceLoaderService: IExtensionResourceLoaderService,
@INotificationService notificationService: INotificationService,
@ILogService logService: ILogService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService
) {
super(modeService, themeService, fileService, notificationService, logService, configurationService, storageService);
super(modeService, themeService, extensionResourceLoaderService, notificationService, logService, configurationService, storageService);
}
protected _loadVSCodeTextmate(): Promise<typeof import('vscode-textmate')> {

View File

@@ -18,7 +18,7 @@ interface ITMGrammarFactoryHost {
export interface ICreateGrammarResult {
languageId: LanguageId;
grammar: IGrammar;
grammar: IGrammar | null;
initialState: StackElement;
containsEmbeddedLanguages: boolean;
}
@@ -102,8 +102,8 @@ export class TMGrammarFactory extends Disposable {
return this._languageToScope2[languageId] ? true : false;
}
public setTheme(theme: IRawTheme): void {
this._grammarRegistry.setTheme(theme);
public setTheme(theme: IRawTheme, colorMap: string[]): void {
this._grammarRegistry.setTheme(theme, colorMap);
}
public getColorMap(): string[] {

View File

@@ -14,7 +14,7 @@ export interface ITextMateService {
onDidEncounterLanguage: Event<LanguageId>;
createGrammar(modeId: string): Promise<IGrammar>;
createGrammar(modeId: string): Promise<IGrammar | null>;
}
// -------------- Types "liberated" from vscode-textmate due to usage in /common/

View File

@@ -8,7 +8,6 @@ import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { AbstractTextMateService } from 'vs/workbench/services/textMate/browser/abstractTextMateService';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { IFileService } from 'vs/platform/files/common/files';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -24,6 +23,7 @@ import { MultilineTokensBuilder } from 'vs/editor/common/model/tokensStore';
import { TMGrammarFactory } from 'vs/workbench/services/textMate/common/TMGrammarFactory';
import { IModelContentChangedEvent } from 'vs/editor/common/model/textModelEvents';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { IExtensionResourceLoaderService } from 'vs/workbench/services/extensionResourceLoader/common/extensionResourceLoader';
const RUN_TEXTMATE_IN_WORKER = false;
@@ -117,14 +117,13 @@ export class TextMateWorkerHost {
constructor(
private readonly textMateService: TextMateService,
@IFileService private readonly _fileService: IFileService
@IExtensionResourceLoaderService private readonly _extensionResourceLoaderService: IExtensionResourceLoaderService,
) {
}
async readFile(_resource: UriComponents): Promise<string> {
const resource = URI.revive(_resource);
const content = await this._fileService.readFile(resource);
return content.value.toString();
return this._extensionResourceLoaderService.readExtensionResource(resource);
}
async setTokens(_resource: UriComponents, versionId: number, tokens: Uint8Array): Promise<void> {
@@ -142,14 +141,14 @@ export class TextMateService extends AbstractTextMateService {
constructor(
@IModeService modeService: IModeService,
@IWorkbenchThemeService themeService: IWorkbenchThemeService,
@IFileService fileService: IFileService,
@IExtensionResourceLoaderService extensionResourceLoaderService: IExtensionResourceLoaderService,
@INotificationService notificationService: INotificationService,
@ILogService logService: ILogService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IModelService private readonly _modelService: IModelService,
) {
super(modeService, themeService, fileService, notificationService, logService, configurationService, storageService);
super(modeService, themeService, extensionResourceLoaderService, notificationService, logService, configurationService, storageService);
this._worker = null;
this._workerProxy = null;
this._tokenizers = Object.create(null);
@@ -190,7 +189,7 @@ export class TextMateService extends AbstractTextMateService {
this._killWorker();
if (RUN_TEXTMATE_IN_WORKER) {
const workerHost = new TextMateWorkerHost(this, this._fileService);
const workerHost = new TextMateWorkerHost(this, this._extensionResourceLoaderService);
const worker = createWebWorker<TextMateWorker>(this._modelService, {
createData: {
grammarDefinitions
@@ -207,18 +206,18 @@ export class TextMateService extends AbstractTextMateService {
return;
}
this._workerProxy = proxy;
if (this._currentTheme) {
this._workerProxy.acceptTheme(this._currentTheme);
if (this._currentTheme && this._currentTokenColorMap) {
this._workerProxy.acceptTheme(this._currentTheme, this._currentTokenColorMap);
}
this._modelService.getModels().forEach((model) => this._onModelAdded(model));
});
}
}
protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme): void {
super._doUpdateTheme(grammarFactory, theme);
if (this._currentTheme && this._workerProxy) {
this._workerProxy.acceptTheme(this._currentTheme);
protected _doUpdateTheme(grammarFactory: TMGrammarFactory, theme: IRawTheme, colorMap: string[]): void {
super._doUpdateTheme(grammarFactory, theme, colorMap);
if (this._currentTheme && this._currentTokenColorMap && this._workerProxy) {
this._workerProxy.acceptTheme(this._currentTheme, this._currentTokenColorMap);
}
}

View File

@@ -185,9 +185,9 @@ export class TextMateWorker {
return this._grammarCache[languageId];
}
public acceptTheme(theme: IRawTheme): void {
public acceptTheme(theme: IRawTheme, colorMap: string[]): void {
if (this._grammarFactory) {
this._grammarFactory.setTheme(theme);
this._grammarFactory.setTheme(theme, colorMap);
}
}

View File

@@ -35,7 +35,7 @@ export class BrowserTextFileService extends AbstractTextFileService {
return false; // no dirty: no veto
}
if (!this.isHotExitEnabled) {
if (!this.filesConfigurationService.isHotExitEnabled) {
return true; // dirty without backup: veto
}
@@ -46,7 +46,7 @@ export class BrowserTextFileService extends AbstractTextFileService {
const model = this.models.get(dirtyResource);
hasBackup = !!(model?.hasBackup());
} else if (dirtyResource.scheme === Schemas.untitled) {
hasBackup = this.untitledEditorService.hasBackup(dirtyResource);
hasBackup = this.untitledTextEditorService.hasBackup(dirtyResource);
}
if (!hasBackup) {

View File

@@ -5,32 +5,28 @@
import * as nls from 'vs/nls';
import { URI } from 'vs/base/common/uri';
import * as errors from 'vs/base/common/errors';
import * as objects from 'vs/base/common/objects';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter, AsyncEmitter } from 'vs/base/common/event';
import * as platform from 'vs/base/common/platform';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, IAutoSaveConfiguration, AutoSaveMode, SaveReason, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ISaveOptions, AutoSaveContext, IWillMoveEvent, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles';
import { ConfirmResult, IRevertOptions } from 'vs/workbench/common/editor';
import { IResult, ITextFileOperationResult, ITextFileService, ITextFileStreamContent, ITextFileEditorModelManager, ITextFileEditorModel, ModelState, ITextFileContent, IResourceEncodings, IReadTextFileOptions, IWriteTextFileOptions, toBufferOrReadable, TextFileOperationError, TextFileOperationResult, FileOperationWillRunEvent, FileOperationDidRunEvent, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { SaveReason, IRevertOptions } from 'vs/workbench/common/editor';
import { ILifecycleService, ShutdownReason, LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { IFileService, IFilesConfiguration, FileOperationError, FileOperationResult, AutoSaveConfiguration, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions } from 'vs/platform/files/common/files';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IFileService, FileOperationError, FileOperationResult, HotExitConfiguration, IFileStatWithMetadata, ICreateFileOptions, FileOperation } from 'vs/platform/files/common/files';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { UntitledTextEditorModel } from 'vs/workbench/common/editor/untitledTextEditorModel';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ResourceMap } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createTextBufferFactoryFromSnapshot, createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { IModelService } from 'vs/editor/common/services/modelService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { isEqualOrParent, isEqual, joinPath, dirname, extname, basename, toLocalResource } from 'vs/base/common/resources';
import { getConfirmMessage, IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation } from 'vs/platform/dialogs/common/dialogs';
import { IDialogService, IFileDialogService, ISaveDialogOptions, IConfirmation, ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { coalesce } from 'vs/base/common/arrays';
@@ -39,6 +35,8 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { ITextSnapshot } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { PLAINTEXT_MODE_ID } from 'vs/editor/common/modes/modesRegistry';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { CancellationToken } from 'vs/base/common/cancellation';
/**
* The workbench file service implementation implements the raw file service spec and adds additional methods on top.
@@ -47,55 +45,42 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
_serviceBrand: undefined;
private readonly _onAutoSaveConfigurationChange: Emitter<IAutoSaveConfiguration> = this._register(new Emitter<IAutoSaveConfiguration>());
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration> = this._onAutoSaveConfigurationChange.event;
//#region events
private readonly _onFilesAssociationChange: Emitter<void> = this._register(new Emitter<void>());
readonly onFilesAssociationChange: Event<void> = this._onFilesAssociationChange.event;
private _onWillRunOperation = this._register(new AsyncEmitter<FileOperationWillRunEvent>());
readonly onWillRunOperation = this._onWillRunOperation.event;
private readonly _onWillMove = this._register(new Emitter<IWillMoveEvent>());
readonly onWillMove: Event<IWillMoveEvent> = this._onWillMove.event;
private _onDidRunOperation = this._register(new Emitter<FileOperationDidRunEvent>());
readonly onDidRunOperation = this._onDidRunOperation.event;
//#endregion
private _models: TextFileEditorModelManager;
get models(): ITextFileEditorModelManager { return this._models; }
abstract get encoding(): IResourceEncodings;
private currentFilesAssociationConfig: { [key: string]: string; };
private configuredAutoSaveDelay?: number;
private configuredAutoSaveOnFocusChange: boolean | undefined;
private configuredAutoSaveOnWindowChange: boolean | undefined;
private configuredHotExit: string | undefined;
private autoSaveContext: IContextKey<string>;
constructor(
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IFileService protected readonly fileService: IFileService,
@IUntitledEditorService protected readonly untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService protected readonly untitledTextEditorService: IUntitledTextEditorService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IModeService private readonly modeService: IModeService,
@IModelService private readonly modelService: IModelService,
@IWorkbenchEnvironmentService protected readonly environmentService: IWorkbenchEnvironmentService,
@INotificationService private readonly notificationService: INotificationService,
@IBackupFileService private readonly backupFileService: IBackupFileService,
@IHistoryService private readonly historyService: IHistoryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IDialogService private readonly dialogService: IDialogService,
@IFileDialogService private readonly fileDialogService: IFileDialogService,
@IEditorService private readonly editorService: IEditorService,
@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService
@ITextResourceConfigurationService protected readonly textResourceConfigurationService: ITextResourceConfigurationService,
@IFilesConfigurationService protected readonly filesConfigurationService: IFilesConfigurationService
) {
super();
this._models = this._register(instantiationService.createInstance(TextFileEditorModelManager));
this.autoSaveContext = AutoSaveContext.bindTo(contextKeyService);
const configuration = configurationService.getValue<IFilesConfiguration>();
this.currentFilesAssociationConfig = configuration?.files?.associations;
this.onFilesConfigurationChange(configuration);
this.registerListeners();
}
@@ -108,12 +93,16 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
this.lifecycleService.onBeforeShutdown(event => event.veto(this.onBeforeShutdown(event.reason)));
this.lifecycleService.onShutdown(this.dispose, this);
// Files configuration changes
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('files')) {
this.onFilesConfigurationChange(this.configurationService.getValue<IFilesConfiguration>());
}
}));
// Auto save changes
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(() => this.onAutoSaveConfigurationChange()));
}
private onAutoSaveConfigurationChange(): void {
// save all dirty when enabling auto save
if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) {
this.saveAll();
}
}
protected onBeforeShutdown(reason: ShutdownReason): boolean | Promise<boolean> {
@@ -124,7 +113,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// If auto save is enabled, save all files and then check again for dirty files
// We DO NOT run any save participant if we are in the shutdown phase for performance reasons
if (this.getAutoSaveMode() !== AutoSaveMode.OFF) {
if (this.filesConfigurationService.getAutoSaveMode() !== AutoSaveMode.OFF) {
return this.saveAll(false /* files only */, { skipSaveParticipants: true }).then(() => {
// If we still have dirty files, we either have untitled ones or files that cannot be saved
@@ -148,7 +137,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
private handleDirtyBeforeShutdown(dirty: URI[], reason: ShutdownReason): boolean | Promise<boolean> {
// If hot exit is enabled, backup dirty files and allow to exit without confirmation
if (this.isHotExitEnabled) {
if (this.filesConfigurationService.isHotExitEnabled) {
return this.backupBeforeShutdown(dirty, reason).then(didBackup => {
if (didBackup) {
return this.noVeto({ cleanUpBackups: false }); // no veto and no backup cleanup (since backup was successful)
@@ -176,7 +165,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
let doBackup: boolean | undefined;
switch (reason) {
case ShutdownReason.CLOSE:
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
} else if (await this.getWindowCount() > 1 || platform.isMacintosh) {
doBackup = false; // do not backup if a window is closed that does not cause quitting of the application
@@ -194,7 +183,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
break;
case ShutdownReason.LOAD:
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.configuredHotExit === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
if (this.contextService.getWorkbenchState() !== WorkbenchState.EMPTY && this.filesConfigurationService.hotExitConfiguration === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
doBackup = true; // backup if a folder is open and onExitAndWindowClose is configured
} else {
doBackup = false; // do not backup because we are switching contexts
@@ -239,18 +228,18 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Handle untitled resources
await Promise.all(untitledResources
.filter(untitled => this.untitledEditorService.exists(untitled))
.map(async untitled => (await this.untitledEditorService.loadOrCreate({ resource: untitled })).backup()));
.filter(untitled => this.untitledTextEditorService.exists(untitled))
.map(async untitled => (await this.untitledTextEditorService.loadOrCreate({ resource: untitled })).backup()));
}
private async confirmBeforeShutdown(): Promise<boolean> {
const confirm = await this.confirmSave();
const confirm = await this.fileDialogService.showSaveConfirm(this.getDirty());
// Save
if (confirm === ConfirmResult.SAVE) {
const result = await this.saveAll(true /* includeUntitled */, { skipSaveParticipants: true });
if (result.results.some(r => !r.success)) {
if (result.results.some(r => r.error)) {
return true; // veto if some saves failed
}
@@ -262,7 +251,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Make sure to revert untitled so that they do not restore
// see https://github.com/Microsoft/vscode/issues/29572
this.untitledEditorService.revertAll();
this.untitledTextEditorService.revertAll();
return this.noVeto({ cleanUpBackups: true });
}
@@ -295,61 +284,6 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
await this.backupFileService.discardAllWorkspaceBackups();
}
protected onFilesConfigurationChange(configuration: IFilesConfiguration): void {
const wasAutoSaveEnabled = (this.getAutoSaveMode() !== AutoSaveMode.OFF);
const autoSaveMode = configuration?.files?.autoSave || AutoSaveConfiguration.OFF;
this.autoSaveContext.set(autoSaveMode);
switch (autoSaveMode) {
case AutoSaveConfiguration.AFTER_DELAY:
this.configuredAutoSaveDelay = configuration?.files?.autoSaveDelay;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = false;
break;
case AutoSaveConfiguration.ON_FOCUS_CHANGE:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = true;
this.configuredAutoSaveOnWindowChange = false;
break;
case AutoSaveConfiguration.ON_WINDOW_CHANGE:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = true;
break;
default:
this.configuredAutoSaveDelay = undefined;
this.configuredAutoSaveOnFocusChange = false;
this.configuredAutoSaveOnWindowChange = false;
break;
}
// Emit as event
this._onAutoSaveConfigurationChange.fire(this.getAutoSaveConfiguration());
// save all dirty when enabling auto save
if (!wasAutoSaveEnabled && this.getAutoSaveMode() !== AutoSaveMode.OFF) {
this.saveAll();
}
// Check for change in files associations
const filesAssociation = configuration?.files?.associations;
if (!objects.equals(this.currentFilesAssociationConfig, filesAssociation)) {
this.currentFilesAssociationConfig = filesAssociation;
this._onFilesAssociationChange.fire();
}
// Hot exit
const hotExitMode = configuration?.files?.hotExit;
if (hotExitMode === HotExitConfiguration.OFF || hotExitMode === HotExitConfiguration.ON_EXIT_AND_WINDOW_CLOSE) {
this.configuredHotExit = hotExitMode;
} else {
this.configuredHotExit = HotExitConfiguration.ON_EXIT;
}
}
//#endregion
//#region primitives (read, create, move, delete, update)
@@ -409,6 +343,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
async create(resource: URI, value?: string | ITextSnapshot, options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
// before event
await this._onWillRunOperation.fireAsync({ operation: FileOperation.CREATE, target: resource }, CancellationToken.None);
const stat = await this.doCreate(resource, value, options);
// If we had an existing model for the given resource, load
@@ -420,6 +358,9 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
await existingModel.revert();
}
// after event
this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.CREATE, resource));
return stat;
}
@@ -432,23 +373,37 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
async delete(resource: URI, options?: { useTrash?: boolean, recursive?: boolean }): Promise<void> {
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
// before event
await this._onWillRunOperation.fireAsync({ operation: FileOperation.DELETE, target: resource }, CancellationToken.None);
const dirtyFiles = this.getDirty().filter(dirty => isEqualOrParent(dirty, resource));
await this.revertAll(dirtyFiles, { soft: true });
return this.fileService.del(resource, options);
await this.fileService.del(resource, options);
// after event
this._onDidRunOperation.fire(new FileOperationDidRunEvent(FileOperation.DELETE, resource));
}
async move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
return this.moveOrCopy(source, target, true, overwrite);
}
// await onWillMove event joiners
await this.notifyOnWillMove(source, target);
async copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata> {
return this.moveOrCopy(source, target, false, overwrite);
}
private async moveOrCopy(source: URI, target: URI, move: boolean, overwrite?: boolean): Promise<IFileStatWithMetadata> {
// before event
await this._onWillRunOperation.fireAsync({ operation: move ? FileOperation.MOVE : FileOperation.COPY, target, source }, CancellationToken.None);
// find all models that related to either source or target (can be many if resource is a folder)
const sourceModels: ITextFileEditorModel[] = [];
const conflictingModels: ITextFileEditorModel[] = [];
for (const model of this.getFileModels()) {
const resource = model.getResource();
const resource = model.resource;
if (isEqualOrParent(resource, target, false /* do not ignorecase, see https://github.com/Microsoft/vscode/issues/56384 */)) {
conflictingModels.push(model);
@@ -464,7 +419,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
type ModelToRestore = { resource: URI; snapshot?: ITextSnapshot };
const modelsToRestore: ModelToRestore[] = [];
for (const sourceModel of sourceModels) {
const sourceModelResource = sourceModel.getResource();
const sourceModelResource = sourceModel.resource;
// If the source is the actual model, just use target as new resource
let modelToRestoreResource: URI;
@@ -486,15 +441,19 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
modelsToRestore.push(modelToRestore);
}
// in order to move, we need to soft revert all dirty models,
// in order to move and copy, we need to soft revert all dirty models,
// both from the source as well as the target if any
const dirtyModels = [...sourceModels, ...conflictingModels].filter(model => model.isDirty());
await this.revertAll(dirtyModels.map(dirtyModel => dirtyModel.getResource()), { soft: true });
await this.revertAll(dirtyModels.map(dirtyModel => dirtyModel.resource), { soft: true });
// now we can rename the source to target via file operation
let stat: IFileStatWithMetadata;
try {
stat = await this.fileService.move(source, target, overwrite);
if (move) {
stat = await this.fileService.move(source, target, overwrite);
} else {
stat = await this.fileService.copy(source, target, overwrite);
}
} catch (error) {
// in case of any error, ensure to set dirty flag back
@@ -521,32 +480,17 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
}));
// after event
this._onDidRunOperation.fire(new FileOperationDidRunEvent(move ? FileOperation.MOVE : FileOperation.COPY, target, source));
return stat;
}
private async notifyOnWillMove(source: URI, target: URI): Promise<void> {
const waitForPromises: Promise<unknown>[] = [];
// fire event
this._onWillMove.fire({
oldResource: source,
newResource: target,
waitUntil(promise: Promise<unknown>) {
waitForPromises.push(promise.then(undefined, errors.onUnexpectedError));
}
});
// prevent async waitUntil-calls
Object.freeze(waitForPromises);
await Promise.all(waitForPromises);
}
//#endregion
//#region save/revert
async save(resource: URI, options?: ISaveOptions): Promise<boolean> {
async save(resource: URI, options?: ITextFileSaveOptions): Promise<boolean> {
// Run a forced save if we detect the file is not dirty so that save participants can still run
if (options?.force && this.fileService.canHandleResource(resource) && !this.isDirty(resource)) {
@@ -560,39 +504,12 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
}
const result = await this.saveAll([resource], options);
return result.results.length === 1 && !!result.results[0].success;
return !(await this.saveAll([resource], options)).results.some(result => result.error);
}
async confirmSave(resources?: URI[]): Promise<ConfirmResult> {
if (this.environmentService.isExtensionDevelopment) {
if (!this.environmentService.args['extension-development-confirm-save']) {
return ConfirmResult.DONT_SAVE; // no veto when we are in extension dev mode because we cannot assume we run interactive (e.g. tests)
}
}
const resourcesToConfirm = this.getDirty(resources);
if (resourcesToConfirm.length === 0) {
return ConfirmResult.DONT_SAVE;
}
return promptSave(this.dialogService, resourcesToConfirm);
}
async confirmOverwrite(resource: URI): Promise<boolean> {
const confirm: IConfirmation = {
message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)),
detail: nls.localize('irreversible', "A file or folder with the same name already exists in the folder {0}. Replacing it will overwrite its current contents.", basename(dirname(resource))),
primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
type: 'warning'
};
return (await this.dialogService.confirm(confirm)).confirmed;
}
saveAll(includeUntitled?: boolean, options?: ISaveOptions): Promise<ITextFileOperationResult>;
saveAll(resources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult>;
saveAll(arg1?: boolean | URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
saveAll(includeUntitled?: boolean, options?: ITextFileSaveOptions): Promise<ITextFileOperationResult>;
saveAll(resources: URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult>;
saveAll(arg1?: boolean | URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult> {
// get all dirty
let toSave: URI[] = [];
@@ -616,7 +533,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return this.doSaveAll(filesToSave, untitledToSave, options);
}
private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ISaveOptions): Promise<ITextFileOperationResult> {
private async doSaveAll(fileResources: URI[], untitledResources: URI[], options?: ITextFileSaveOptions): Promise<ITextFileOperationResult> {
// Handle files first that can just be saved
const result = await this.doSaveAllFiles(fileResources, options);
@@ -624,11 +541,11 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// Preflight for untitled to handle cancellation from the dialog
const targetsForUntitled: URI[] = [];
for (const untitled of untitledResources) {
if (this.untitledEditorService.exists(untitled)) {
if (this.untitledTextEditorService.exists(untitled)) {
let targetUri: URI;
// Untitled with associated file path don't need to prompt
if (this.untitledEditorService.hasAssociatedFilePath(untitled)) {
if (this.untitledTextEditorService.hasAssociatedFilePath(untitled)) {
targetUri = toLocalResource(untitled, this.environmentService.configuration.remoteAuthority);
}
@@ -653,7 +570,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
result.results.push({
source: untitledResources[index],
target: uri,
success: !!uri
error: !uri // the operation was canceled or failed, so mark as error
});
}));
@@ -721,7 +638,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return options;
}
private async doSaveAllFiles(resources?: URI[], options: ISaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
private async doSaveAllFiles(resources?: URI[], options: ITextFileSaveOptions = Object.create(null)): Promise<ITextFileOperationResult> {
const dirtyFileModels = this.getDirtyFileModels(Array.isArray(resources) ? resources : undefined /* Save All */)
.filter(model => {
if ((model.hasState(ModelState.CONFLICT) || model.hasState(ModelState.ERROR)) && (options.reason === SaveReason.AUTO || options.reason === SaveReason.FOCUS_CHANGE || options.reason === SaveReason.WINDOW_CHANGE)) {
@@ -732,19 +649,20 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
});
const mapResourceToResult = new ResourceMap<IResult>();
dirtyFileModels.forEach(m => {
mapResourceToResult.set(m.getResource(), {
source: m.getResource()
dirtyFileModels.forEach(dirtyModel => {
mapResourceToResult.set(dirtyModel.resource, {
source: dirtyModel.resource
});
});
await Promise.all(dirtyFileModels.map(async model => {
await model.save(options);
if (!model.isDirty()) {
const result = mapResourceToResult.get(model.getResource());
// If model is still dirty, mark the resulting operation as error
if (model.isDirty()) {
const result = mapResourceToResult.get(model.resource);
if (result) {
result.success = true;
result.error = true;
}
}
}));
@@ -769,7 +687,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return this.getFileModels(resources).filter(model => model.isDirty());
}
async saveAs(resource: URI, targetResource?: URI, options?: ISaveOptions): Promise<URI | undefined> {
async saveAs(resource: URI, targetResource?: URI, options?: ITextFileSaveOptions): Promise<URI | undefined> {
// Get to target resource
if (!targetResource) {
@@ -796,14 +714,14 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return this.doSaveAs(resource, targetResource, options);
}
private async doSaveAs(resource: URI, target: URI, options?: ISaveOptions): Promise<URI> {
private async doSaveAs(resource: URI, target: URI, options?: ITextFileSaveOptions): Promise<URI> {
// Retrieve text model from provided resource if any
let model: ITextFileEditorModel | UntitledEditorModel | undefined;
let model: ITextFileEditorModel | UntitledTextEditorModel | undefined;
if (this.fileService.canHandleResource(resource)) {
model = this._models.get(resource);
} else if (resource.scheme === Schemas.untitled && this.untitledEditorService.exists(resource)) {
model = await this.untitledEditorService.loadOrCreate({ resource });
} else if (resource.scheme === Schemas.untitled && this.untitledTextEditorService.exists(resource)) {
model = await this.untitledTextEditorService.loadOrCreate({ resource });
}
// We have a model: Use it (can be null e.g. if this file is binary and not a text file or was never opened before)
@@ -830,7 +748,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
return target;
}
private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledEditorModel, resource: URI, target: URI, options?: ISaveOptions): Promise<boolean> {
private async doSaveTextFileAs(sourceModel: ITextFileEditorModel | UntitledTextEditorModel, resource: URI, target: URI, options?: ITextFileSaveOptions): Promise<boolean> {
// Prefer an existing model if it is already loaded for the given target resource
let targetExists: boolean = false;
@@ -858,7 +776,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
// path. This can happen if the file was created after the untitled file was opened.
// See https://github.com/Microsoft/vscode/issues/67946
let write: boolean;
if (sourceModel instanceof UntitledEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.getResource(), this.environmentService.configuration.remoteAuthority))) {
if (sourceModel instanceof UntitledTextEditorModel && sourceModel.hasAssociatedFilePath && targetExists && isEqual(target, toLocalResource(sourceModel.resource, this.environmentService.configuration.remoteAuthority))) {
write = await this.confirmOverwrite(target);
} else {
write = true;
@@ -900,8 +818,19 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
}
private async confirmOverwrite(resource: URI): Promise<boolean> {
const confirm: IConfirmation = {
message: nls.localize('confirmOverwrite', "'{0}' already exists. Do you want to replace it?", basename(resource)),
detail: nls.localize('irreversible', "A file or folder with the name '{0}' already exists in the folder '{1}'. Replacing it will overwrite its current contents.", basename(resource), basename(dirname(resource))),
primaryButton: nls.localize({ key: 'replaceButtonLabel', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
type: 'warning'
};
return (await this.dialogService.confirm(confirm)).confirmed;
}
private suggestFileName(untitledResource: URI): URI {
const untitledFileName = this.untitledEditorService.suggestFileName(untitledResource);
const untitledFileName = this.untitledTextEditorService.suggestFileName(untitledResource);
const remoteAuthority = this.environmentService.configuration.remoteAuthority;
const schemeFilter = remoteAuthority ? Schemas.vscodeRemote : Schemas.file;
@@ -920,9 +849,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
async revert(resource: URI, options?: IRevertOptions): Promise<boolean> {
const result = await this.revertAll([resource], options);
return result.results.length === 1 && !!result.results[0].success;
return !(await this.revertAll([resource], options)).results.some(result => result.error);
}
async revertAll(resources?: URI[], options?: IRevertOptions): Promise<ITextFileOperationResult> {
@@ -931,8 +858,8 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
const revertOperationResult = await this.doRevertAllFiles(resources, options);
// Revert untitled
const untitledReverted = this.untitledEditorService.revertAll(resources);
untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled, success: true }));
const untitledReverted = this.untitledTextEditorService.revertAll(resources);
untitledReverted.forEach(untitled => revertOperationResult.results.push({ source: untitled }));
return revertOperationResult;
}
@@ -941,30 +868,28 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
const fileModels = options?.force ? this.getFileModels(resources) : this.getDirtyFileModels(resources);
const mapResourceToResult = new ResourceMap<IResult>();
fileModels.forEach(m => {
mapResourceToResult.set(m.getResource(), {
source: m.getResource()
fileModels.forEach(fileModel => {
mapResourceToResult.set(fileModel.resource, {
source: fileModel.resource
});
});
await Promise.all(fileModels.map(async model => {
try {
await model.revert(options?.soft);
await model.revert(options);
if (!model.isDirty()) {
const result = mapResourceToResult.get(model.getResource());
// If model is still dirty, mark the resulting operation as error
if (model.isDirty()) {
const result = mapResourceToResult.get(model.resource);
if (result) {
result.success = true;
result.error = true;
}
}
} catch (error) {
// FileNotFound means the file got deleted meanwhile, so still record as successful revert
// FileNotFound means the file got deleted meanwhile, so ignore it
if ((<FileOperationError>error).fileOperationResult === FileOperationResult.FILE_NOT_FOUND) {
const result = mapResourceToResult.get(model.getResource());
if (result) {
result.success = true;
}
return;
}
// Otherwise bubble up the error
@@ -980,10 +905,10 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
getDirty(resources?: URI[]): URI[] {
// Collect files
const dirty = this.getDirtyFileModels(resources).map(m => m.getResource());
const dirty = this.getDirtyFileModels(resources).map(dirtyFileModel => dirtyFileModel.resource);
// Add untitled ones
dirty.push(...this.untitledEditorService.getDirty(resources));
dirty.push(...this.untitledTextEditorService.getDirty(resources));
return dirty;
}
@@ -996,39 +921,7 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
}
// Check for dirty untitled
return this.untitledEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
}
//#endregion
//#region config
getAutoSaveMode(): AutoSaveMode {
if (this.configuredAutoSaveOnFocusChange) {
return AutoSaveMode.ON_FOCUS_CHANGE;
}
if (this.configuredAutoSaveOnWindowChange) {
return AutoSaveMode.ON_WINDOW_CHANGE;
}
if (this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0) {
return this.configuredAutoSaveDelay <= 1000 ? AutoSaveMode.AFTER_SHORT_DELAY : AutoSaveMode.AFTER_LONG_DELAY;
}
return AutoSaveMode.OFF;
}
getAutoSaveConfiguration(): IAutoSaveConfiguration {
return {
autoSaveDelay: this.configuredAutoSaveDelay && this.configuredAutoSaveDelay > 0 ? this.configuredAutoSaveDelay : undefined,
autoSaveFocusChange: !!this.configuredAutoSaveOnFocusChange,
autoSaveApplicationChange: !!this.configuredAutoSaveOnWindowChange
};
}
get isHotExitEnabled(): boolean {
return !this.environmentService.isExtensionDevelopment && this.configuredHotExit !== HotExitConfiguration.OFF;
return this.untitledTextEditorService.getDirty().some(dirty => !resource || dirty.toString() === resource.toString());
}
//#endregion
@@ -1041,26 +934,3 @@ export abstract class AbstractTextFileService extends Disposable implements ITex
super.dispose();
}
}
export async function promptSave(dialogService: IDialogService, resourcesToConfirm: readonly URI[]) {
const message = resourcesToConfirm.length === 1
? nls.localize('saveChangesMessage', "Do you want to save the changes you made to {0}?", basename(resourcesToConfirm[0]))
: getConfirmMessage(nls.localize('saveChangesMessages', "Do you want to save the changes to the following {0} files?", resourcesToConfirm.length), resourcesToConfirm);
const buttons: string[] = [
resourcesToConfirm.length > 1 ? nls.localize({ key: 'saveAll', comment: ['&& denotes a mnemonic'] }, "&&Save All") : nls.localize({ key: 'save', comment: ['&& denotes a mnemonic'] }, "&&Save"),
nls.localize({ key: 'dontSave', comment: ['&& denotes a mnemonic'] }, "Do&&n't Save"),
nls.localize('cancel', "Cancel")
];
const { choice } = await dialogService.show(Severity.Warning, message, buttons, {
cancelId: 2,
detail: nls.localize('saveChangesDetail', "Your changes will be lost if you don't save them.")
});
switch (choice) {
case 0: return ConfirmResult.SAVE;
case 1: return ConfirmResult.DONT_SAVE;
default: return ConfirmResult.CANCEL;
}
}

View File

@@ -4,18 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter } from 'vs/base/common/event';
import { guessMimeTypes } from 'vs/base/common/mime';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { URI } from 'vs/base/common/uri';
import { isUndefinedOrNull, assertIsDefined } from 'vs/base/common/types';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ITextFileService, IAutoSaveConfiguration, ModelState, ITextFileEditorModel, ISaveOptions, ISaveErrorHandler, ISaveParticipant, StateChange, SaveReason, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode } from 'vs/workbench/common/editor';
import { ITextFileService, ModelState, ITextFileEditorModel, ISaveErrorHandler, ISaveParticipant, StateChange, ITextFileStreamContent, ILoadOptions, LoadReason, IResolvedTextFileEditorModel, ITextFileSaveOptions } from 'vs/workbench/services/textfile/common/textfiles';
import { EncodingMode, IRevertOptions, SaveReason } from 'vs/workbench/common/editor';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED } from 'vs/platform/files/common/files';
import { IFileService, FileOperationError, FileOperationResult, CONTENT_CHANGE_EVENT_BUFFER_DELAY, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
@@ -29,9 +29,12 @@ import { ILogService } from 'vs/platform/log/common/log';
import { isEqual, isEqualOrParent, extname, basename, joinPath } from 'vs/base/common/resources';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Schemas } from 'vs/base/common/network';
import { IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { IFilesConfigurationService, IAutoSaveConfiguration } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
export interface IBackupMetaData {
mtime: number;
ctime: number;
size: number;
etag: string;
orphaned: boolean;
@@ -69,11 +72,16 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private static saveParticipant: ISaveParticipant | null;
static setSaveParticipant(handler: ISaveParticipant | null): void { TextFileEditorModel.saveParticipant = handler; }
private readonly _onDidContentChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
readonly onDidContentChange: Event<StateChange> = this._onDidContentChange.event;
private readonly _onDidContentChange = this._register(new Emitter<StateChange>());
readonly onDidContentChange = this._onDidContentChange.event;
private readonly _onDidStateChange: Emitter<StateChange> = this._register(new Emitter<StateChange>());
readonly onDidStateChange: Event<StateChange> = this._onDidStateChange.event;
private readonly _onDidStateChange = this._register(new Emitter<StateChange>());
readonly onDidStateChange = this._onDidStateChange.event;
private readonly _onDidChangeDirty = this._register(new Emitter<void>());
readonly onDidChangeDirty = this._onDidChangeDirty.event;
readonly capabilities = WorkingCopyCapabilities.AutoSave;
private contentEncoding: string | undefined; // encoding as reported from disk
@@ -100,7 +108,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
private disposed = false;
constructor(
private resource: URI,
public readonly resource: URI,
private preferredEncoding: string | undefined, // encoding as chosen by the user
private preferredMode: string | undefined, // mode as chosen by the user
@INotificationService private readonly notificationService: INotificationService,
@@ -113,19 +121,24 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
@IBackupFileService private readonly backupFileService: IBackupFileService,
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@IWorkingCopyService private readonly workingCopyService: IWorkingCopyService,
@IFilesConfigurationService private readonly filesConfigurationService: IFilesConfigurationService
) {
super(modelService, modeService);
this.updateAutoSaveConfiguration(textFileService.getAutoSaveConfiguration());
this.updateAutoSaveConfiguration(filesConfigurationService.getAutoSaveConfiguration());
// Make known to working copy service
this._register(this.workingCopyService.registerWorkingCopy(this));
this.registerListeners();
}
private registerListeners(): void {
this._register(this.fileService.onFileChanges(e => this.onFileChanges(e)));
this._register(this.textFileService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
this._register(this.textFileService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
this._register(this.filesConfigurationService.onAutoSaveConfigurationChange(config => this.updateAutoSaveConfiguration(config)));
this._register(this.filesConfigurationService.onFilesAssociationChange(e => this.onFilesAssociationChange()));
this._register(this.onDidStateChange(e => this.onStateChange(e)));
}
@@ -224,6 +237,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
if (isEqual(target, this.resource) && this.lastResolvedFileStat) {
meta = {
mtime: this.lastResolvedFileStat.mtime,
ctime: this.lastResolvedFileStat.ctime,
size: this.lastResolvedFileStat.size,
etag: this.lastResolvedFileStat.etag,
orphaned: this.inOrphanMode
@@ -238,19 +252,21 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return this.backupFileService.hasBackupSync(this.resource, this.versionId);
}
async revert(soft?: boolean): Promise<void> {
async revert(options?: IRevertOptions): Promise<boolean> {
if (!this.isResolved()) {
return;
return false;
}
// Cancel any running auto-save
this.autoSaveDisposable.clear();
// Unset flags
const wasDirty = this.dirty;
const undo = this.setDirty(false);
// Force read from disk unless reverting soft
if (!soft) {
const softUndo = options?.soft;
if (!softUndo) {
try {
await this.load({ forceReadFromDisk: true });
} catch (error) {
@@ -264,6 +280,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit file change event
this._onDidStateChange.fire(StateChange.REVERTED);
// Emit dirty change event
if (wasDirty) {
this._onDidChangeDirty.fire();
}
return true;
}
async load(options?: ILoadOptions): Promise<ITextFileEditorModel> {
@@ -313,11 +336,11 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
resource: this.resource,
name: basename(this.resource),
mtime: resolvedBackup.meta ? resolvedBackup.meta.mtime : Date.now(),
ctime: resolvedBackup.meta ? resolvedBackup.meta.ctime : Date.now(),
size: resolvedBackup.meta ? resolvedBackup.meta.size : 0,
etag: resolvedBackup.meta ? resolvedBackup.meta.etag : ETAG_DISABLED, // etag disabled if unknown!
value: resolvedBackup.value,
encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding,
isReadonly: false
encoding: this.textFileService.encoding.getPreferredWriteEncoding(this.resource, this.preferredEncoding).encoding
}, options, true /* from backup */);
// Restore orphaned flag based on state
@@ -397,11 +420,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
resource: this.resource,
name: content.name,
mtime: content.mtime,
ctime: content.ctime,
size: content.size,
etag: content.etag,
isFile: true,
isDirectory: false,
isSymbolicLink: false,
isReadonly: content.isReadonly
isSymbolicLink: false
});
// Keep the original encoding to not loose it when saving
@@ -523,6 +547,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit event
if (wasDirty) {
this._onDidStateChange.fire(StateChange.REVERTED);
this._onDidChangeDirty.fire();
}
return;
@@ -563,6 +588,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Emit as Event if we turned dirty
if (!wasDirty) {
this._onDidStateChange.fire(StateChange.DIRTY);
this._onDidChangeDirty.fire();
}
}
@@ -587,9 +613,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
this.autoSaveDisposable.value = toDisposable(() => clearTimeout(handle));
}
async save(options: ISaveOptions = Object.create(null)): Promise<void> {
async save(options: ITextFileSaveOptions = Object.create(null)): Promise<boolean> {
if (!this.isResolved()) {
return;
return false;
}
this.logService.trace('save() - enter', this.resource);
@@ -597,10 +623,12 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Cancel any currently running auto saves to make this the one that succeeds
this.autoSaveDisposable.clear();
return this.doSave(this.versionId, options);
await this.doSave(this.versionId, options);
return true;
}
private doSave(versionId: number, options: ISaveOptions): Promise<void> {
private doSave(versionId: number, options: ITextFileSaveOptions): Promise<void> {
if (isUndefinedOrNull(options.reason)) {
options.reason = SaveReason.EXPLICIT;
}
@@ -734,8 +762,9 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Cancel any content change event promises as they are no longer valid
this.contentChangeEventScheduler.cancel();
// Emit File Saved Event
// Emit Events
this._onDidStateChange.fire(StateChange.SAVED);
this._onDidChangeDirty.fire();
// Telemetry
const settingsType = this.getTypeIfSettings();
@@ -1001,17 +1030,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
}
isReadonly(): boolean {
return !!(this.lastResolvedFileStat && this.lastResolvedFileStat.isReadonly);
return this.fileService.hasCapability(this.resource, FileSystemProviderCapabilities.Readonly);
}
isDisposed(): boolean {
return this.disposed;
}
getResource(): URI {
return this.resource;
}
getStat(): IFileStatWithMetadata | undefined {
return this.lastResolvedFileStat;
}
@@ -1122,6 +1147,6 @@ class DefaultSaveErrorHandler implements ISaveErrorHandler {
constructor(@INotificationService private readonly notificationService: INotificationService) { }
onSaveError(error: Error, model: TextFileEditorModel): void {
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.getResource()), toErrorMessage(error, false)));
this.notificationService.error(nls.localize('genericSaveError', "Failed to save '{0}': {1}", basename(model.resource), toErrorMessage(error, false)));
}
}

View File

@@ -282,15 +282,15 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
this.mapResourceToModel.clear();
this.mapResourceToPendingModelLoaders.clear();
// dispose dispose listeners
// dispose the dispose listeners
this.mapResourceToDisposeListener.forEach(l => l.dispose());
this.mapResourceToDisposeListener.clear();
// dispose state change listeners
// dispose the state change listeners
this.mapResourceToStateChangeListener.forEach(l => l.dispose());
this.mapResourceToStateChangeListener.clear();
// dispose model content change listeners
// dispose the model content change listeners
this.mapResourceToModelContentChangeListener.forEach(l => l.dispose());
this.mapResourceToModelContentChangeListener.clear();
}
@@ -304,7 +304,7 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE
return; // already disposed
}
if (this.mapResourceToPendingModelLoaders.has(model.getResource())) {
if (this.mapResourceToPendingModelLoaders.has(model.resource)) {
return; // not yet loaded
}

View File

@@ -4,17 +4,17 @@
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { Event } from 'vs/base/common/event';
import { Event, IWaitUntil } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IEncodingSupport, ConfirmResult, IRevertOptions, IModeSupport } from 'vs/workbench/common/editor';
import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { IEncodingSupport, IModeSupport, ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor';
import { IBaseStatWithMetadata, IFileStatWithMetadata, IReadFileOptions, IWriteFileOptions, FileOperationError, FileOperationResult, FileOperation } from 'vs/platform/files/common/files';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { ITextEditorModel } from 'vs/editor/common/services/resolverService';
import { ITextBufferFactory, ITextModel, ITextSnapshot } from 'vs/editor/common/model';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { VSBuffer, VSBufferReadable } from 'vs/base/common/buffer';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { isNative } from 'vs/base/common/platform';
import { IWorkingCopy } from 'vs/workbench/services/workingCopy/common/workingCopyService';
export const ITextFileService = createDecorator<ITextFileService>('textFileService');
@@ -22,13 +22,15 @@ export interface ITextFileService extends IDisposable {
_serviceBrand: undefined;
readonly onWillMove: Event<IWillMoveEvent>;
/**
* An event that is fired before attempting a certain file operation.
*/
readonly onWillRunOperation: Event<FileOperationWillRunEvent>;
readonly onAutoSaveConfigurationChange: Event<IAutoSaveConfiguration>;
readonly onFilesAssociationChange: Event<void>;
readonly isHotExitEnabled: boolean;
/**
* An event that is fired after a file operation has been performed.
*/
readonly onDidRunOperation: Event<FileOperationDidRunEvent>;
/**
* Access to the manager of text file editor models providing further methods to work with them.
@@ -129,22 +131,24 @@ export interface ITextFileService extends IDisposable {
move(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
/**
* Brings up the confirm dialog to either save, don't save or cancel.
*
* @param resources the resources of the files to ask for confirmation or null if
* confirming for all dirty resources.
* Copy a file. If the file is dirty, its contents will be preserved and restored.
*/
confirmSave(resources?: URI[]): Promise<ConfirmResult>;
copy(source: URI, target: URI, overwrite?: boolean): Promise<IFileStatWithMetadata>;
}
/**
* Convenient fast access to the current auto save mode.
*/
getAutoSaveMode(): AutoSaveMode;
export interface FileOperationWillRunEvent extends IWaitUntil {
operation: FileOperation;
target: URI;
source?: URI;
}
/**
* Convenient fast access to the raw configured auto save settings.
*/
getAutoSaveConfiguration(): IAutoSaveConfiguration;
export class FileOperationDidRunEvent {
constructor(
readonly operation: FileOperation,
readonly target: URI,
readonly source?: URI | undefined
) { }
}
export interface IReadTextFileOptions extends IReadFileOptions {
@@ -291,7 +295,7 @@ export class TextFileModelChangeEvent {
private _resource: URI;
constructor(model: ITextFileEditorModel, private _kind: StateChange) {
this._resource = model.getResource();
this._resource = model.resource;
}
get resource(): URI {
@@ -303,8 +307,6 @@ export class TextFileModelChangeEvent {
}
}
export const AutoSaveContext = new RawContextKey<string>('config.files.autoSave', undefined);
export interface ITextFileOperationResult {
results: IResult[];
}
@@ -312,28 +314,7 @@ export interface ITextFileOperationResult {
export interface IResult {
source: URI;
target?: URI;
success?: boolean;
}
export interface IAutoSaveConfiguration {
autoSaveDelay?: number;
autoSaveFocusChange: boolean;
autoSaveApplicationChange: boolean;
}
export const enum AutoSaveMode {
OFF,
AFTER_SHORT_DELAY,
AFTER_LONG_DELAY,
ON_FOCUS_CHANGE,
ON_WINDOW_CHANGE
}
export const enum SaveReason {
EXPLICIT = 1,
AUTO = 2,
FOCUS_CHANGE = 3,
WINDOW_CHANGE = 4
error?: boolean;
}
export const enum LoadReason {
@@ -427,14 +408,10 @@ export interface ITextFileEditorModelManager {
disposeModel(model: ITextFileEditorModel): void;
}
export interface ISaveOptions {
force?: boolean;
reason?: SaveReason;
export interface ITextFileSaveOptions extends ISaveOptions {
overwriteReadonly?: boolean;
overwriteEncoding?: boolean;
skipSaveParticipants?: boolean;
writeElevated?: boolean;
availableFileSystems?: readonly string[];
}
export interface ILoadOptions {
@@ -455,22 +432,20 @@ export interface ILoadOptions {
reason?: LoadReason;
}
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport {
export interface ITextFileEditorModel extends ITextEditorModel, IEncodingSupport, IModeSupport, IWorkingCopy {
readonly onDidContentChange: Event<StateChange>;
readonly onDidStateChange: Event<StateChange>;
getResource(): URI;
hasState(state: ModelState): boolean;
updatePreferredEncoding(encoding: string | undefined): void;
save(options?: ISaveOptions): Promise<void>;
save(options?: ITextFileSaveOptions): Promise<boolean>;
load(options?: ILoadOptions): Promise<ITextFileEditorModel>;
revert(soft?: boolean): Promise<void>;
revert(options?: IRevertOptions): Promise<boolean>;
backup(target?: URI): Promise<void>;
@@ -492,13 +467,6 @@ export interface IResolvedTextFileEditorModel extends ITextFileEditorModel {
createSnapshot(): ITextSnapshot;
}
export interface IWillMoveEvent {
oldResource: URI;
newResource: URI;
waitUntil(p: Promise<unknown>): void;
}
/**
* Helper method to convert a snapshot into its full string form.
*/

View File

@@ -14,7 +14,7 @@ import { Schemas } from 'vs/base/common/network';
import { exists, stat, chmod, rimraf, MAX_FILE_SIZE, MAX_HEAP_SIZE } from 'vs/base/node/pfs';
import { join, dirname } from 'vs/base/common/path';
import { isMacintosh } from 'vs/base/common/platform';
import product from 'vs/platform/product/common/product';
import { IProductService } from 'vs/platform/product/common/productService';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/resourceConfiguration';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { UTF8, UTF8_with_bom, UTF16be, UTF16le, encodingExists, encodeStream, UTF8_BOM, toDecodeStream, IDecodeStreamResult, detectEncodingByBOMFromBuffer, isUTFEncoding } from 'vs/base/node/encoding';
@@ -22,49 +22,49 @@ import { WORKSPACE_EXTENSION } from 'vs/platform/workspaces/common/workspaces';
import { joinPath, extname, isEqualOrParent } from 'vs/base/common/resources';
import { Disposable } from 'vs/base/common/lifecycle';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { VSBufferReadable } from 'vs/base/common/buffer';
import { VSBufferReadable, bufferToStream } from 'vs/base/common/buffer';
import { Readable } from 'stream';
import { createTextBufferFactoryFromStream } from 'vs/editor/common/model/textModel';
import { ITextSnapshot } from 'vs/editor/common/model';
import { nodeReadableToString, streamToNodeReadable, nodeStreamToVSBufferReadable } from 'vs/base/node/stream';
import { IElectronService } from 'vs/platform/electron/node/electron';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
import { ILifecycleService } from 'vs/platform/lifecycle/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IModeService } from 'vs/editor/common/services/modeService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
import { IHistoryService } from 'vs/workbench/services/history/common/history';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { assign } from 'vs/base/common/objects';
import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
export class NativeTextFileService extends AbstractTextFileService {
constructor(
@IWorkspaceContextService contextService: IWorkspaceContextService,
@IFileService fileService: IFileService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@ILifecycleService lifecycleService: ILifecycleService,
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
@IModeService modeService: IModeService,
@IModelService modelService: IModelService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@INotificationService notificationService: INotificationService,
@IBackupFileService backupFileService: IBackupFileService,
@IHistoryService historyService: IHistoryService,
@IContextKeyService contextKeyService: IContextKeyService,
@IDialogService dialogService: IDialogService,
@IFileDialogService fileDialogService: IFileDialogService,
@IEditorService editorService: IEditorService,
@ITextResourceConfigurationService textResourceConfigurationService: ITextResourceConfigurationService,
@IElectronService private readonly electronService: IElectronService
@IElectronService private readonly electronService: IElectronService,
@IProductService private readonly productService: IProductService,
@IFilesConfigurationService filesConfigurationService: IFilesConfigurationService
) {
super(contextService, fileService, untitledEditorService, lifecycleService, instantiationService, configurationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, contextKeyService, dialogService, fileDialogService, editorService, textResourceConfigurationService);
super(contextService, fileService, untitledTextEditorService, lifecycleService, instantiationService, modeService, modelService, environmentService, notificationService, backupFileService, historyService, dialogService, fileDialogService, editorService, textResourceConfigurationService, filesConfigurationService);
}
private _encoding: EncodingOracle | undefined;
@@ -77,7 +77,16 @@ export class NativeTextFileService extends AbstractTextFileService {
}
async read(resource: URI, options?: IReadTextFileOptions): Promise<ITextFileContent> {
const [bufferStream, decoder] = await this.doRead(resource, options);
const [bufferStream, decoder] = await this.doRead(resource,
assign({
// optimization: since we know that the caller does not
// care about buffering, we indicate this to the reader.
// this reduces all the overhead the buffered reading
// has (open, read, close) if the provider supports
// unbuffered reading.
preferUnbuffered: true
}, options || Object.create(null))
);
return {
...bufferStream,
@@ -96,13 +105,22 @@ export class NativeTextFileService extends AbstractTextFileService {
};
}
private async doRead(resource: URI, options?: IReadTextFileOptions): Promise<[IFileStreamContent, IDecodeStreamResult]> {
private async doRead(resource: URI, options?: IReadTextFileOptions & { preferUnbuffered?: boolean }): Promise<[IFileStreamContent, IDecodeStreamResult]> {
// ensure limits
options = this.ensureLimits(options);
// read stream raw
const bufferStream = await this.fileService.readFileStream(resource, options);
// read stream raw (either buffered or unbuffered)
let bufferStream: IFileStreamContent;
if (options.preferUnbuffered) {
const content = await this.fileService.readFile(resource, options);
bufferStream = {
...content,
value: bufferToStream(content.value)
};
} else {
bufferStream = await this.fileService.readFileStream(resource, options);
}
// read through encoding library
const decoder = await toDecodeStream(streamToNodeReadable(bufferStream.value), {
@@ -272,8 +290,8 @@ export class NativeTextFileService extends AbstractTextFileService {
return new Promise<void>((resolve, reject) => {
const promptOptions = {
name: this.environmentService.appNameLong.replace('-', ''),
icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${product.nameShort}.icns`) : undefined
name: this.productService.nameLong.replace('-', ''),
icns: (isMacintosh && this.environmentService.isBuilt) ? join(dirname(this.environmentService.appRoot), `${this.productService.nameShort}.icns`) : undefined
};
const sudoCommand: string[] = [`"${this.environmentService.cliPath}"`];
@@ -357,7 +375,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
if (!overwriteEncoding && encoding === UTF8) {
try {
const buffer = (await this.fileService.readFile(resource, { length: UTF8_BOM.length })).value;
if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8) {
if (detectEncodingByBOMFromBuffer(buffer, buffer.byteLength) === UTF8_with_bom) {
return { encoding, addBOM: true };
}
} catch (error) {
@@ -382,7 +400,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
// Encoding passed in as option
if (options?.encoding) {
if (detectedEncoding === UTF8 && options.encoding === UTF8) {
if (detectedEncoding === UTF8_with_bom && options.encoding === UTF8) {
preferredEncoding = UTF8_with_bom; // indicate the file has BOM if we are to resolve with UTF 8
} else {
preferredEncoding = options.encoding; // give passed in encoding highest priority
@@ -391,11 +409,7 @@ export class EncodingOracle extends Disposable implements IResourceEncodings {
// Encoding detected
else if (detectedEncoding) {
if (detectedEncoding === UTF8) {
preferredEncoding = UTF8_with_bom; // if we detected UTF-8, it can only be because of a BOM
} else {
preferredEncoding = detectedEncoding;
}
preferredEncoding = detectedEncoding;
}
// Encoding configured

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