mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-11 18:48:33 -05:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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))!);
|
||||
}
|
||||
|
||||
@@ -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[]> {
|
||||
|
||||
12
src/vs/workbench/services/backup/electron-browser/backup.ts
Normal file
12
src/vs/workbench/services/backup/electron-browser/backup.ts
Normal 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))!);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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() };
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 []; }
|
||||
|
||||
236
src/vs/workbench/services/remote/common/remoteExplorerService.ts
Normal file
236
src/vs/workbench/services/remote/common/remoteExplorerService.ts
Normal 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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
355
src/vs/workbench/services/search/common/textSearchManager.ts
Normal file
355
src/vs/workbench/services/search/common/textSearchManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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]
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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')> {
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user