chore(vscode): update to 1.53.2

These conflicts will be resolved in the following commits. We do it this way so
that PR review is possible.
This commit is contained in:
Joe Previte
2021-02-25 11:27:27 -07:00
1900 changed files with 83066 additions and 64589 deletions

View File

@@ -17,6 +17,7 @@ import { LanguageConfigurationFileHandler } from 'vs/workbench/contrib/codeEdito
// --- mainThread participants
import './mainThreadBulkEdits';
import './mainThreadCodeInsets';
import './mainThreadCLICommands';
import './mainThreadClipboard';
import './mainThreadCommands';
import './mainThreadConfiguration';
@@ -30,6 +31,7 @@ import './mainThreadDocuments';
import './mainThreadDocumentsAndEditors';
import './mainThreadEditor';
import './mainThreadEditors';
import './mainThreadEditorTabs';
import './mainThreadErrors';
import './mainThreadExtensionService';
import './mainThreadFileSystem';
@@ -54,6 +56,7 @@ import './mainThreadTheming';
import './mainThreadTreeViews';
import './mainThreadDownloadService';
import './mainThreadUrls';
import './mainThreadUriOpeners';
import './mainThreadWindow';
import './mainThreadWebviewManager';
import './mainThreadWorkspace';
@@ -65,6 +68,7 @@ import './mainThreadTunnelService';
import './mainThreadAuthentication';
import './mainThreadTimeline';
import './mainThreadTesting';
import './mainThreadSecretState';
import 'vs/workbench/api/common/apiCommands';
export class ExtensionPoints implements IWorkbenchContribution {

View File

@@ -18,9 +18,6 @@ import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteA
import { fromNow } from 'vs/base/common/date';
import { ActivationKind, IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { isWeb } from 'vs/base/common/platform';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
import { IProductService } from 'vs/platform/product/common/productService';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
const VSO_ALLOWED_EXTENSIONS = ['github.vscode-pull-request-github', 'github.vscode-pull-request-github-insiders', 'vscode.git', 'ms-vsonline.vsonline', 'vscode.github-browser', 'ms-vscode.github-browser', 'github.codespaces'];
@@ -225,10 +222,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
@INotificationService private readonly notificationService: INotificationService,
@IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IExtensionService private readonly extensionService: IExtensionService,
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IEncryptionService private readonly encryptionService: IEncryptionService,
@IProductService private readonly productService: IProductService
@IExtensionService private readonly extensionService: IExtensionService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAuthentication);
@@ -250,10 +244,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this._register(this.authenticationService.onDidChangeDeclaredProviders(e => {
this._proxy.$setProviders(e);
}));
this._register(this.credentialsService.onDidChangePassword(_ => {
this._proxy.$onDidChangePassword();
}));
}
$getProviderIds(): Promise<string[]> {
@@ -416,7 +406,7 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
const remoteConnection = this.remoteAgentService.getConnection();
const isVSO = remoteConnection !== null
? remoteConnection.remoteAuthority.startsWith('vsonline')
? remoteConnection.remoteAuthority.startsWith('vsonline') || remoteConnection.remoteAuthority.startsWith('codespaces')
: isWeb;
if (isVSO && VSO_ALLOWED_EXTENSIONS.includes(extensionId)) {
@@ -466,46 +456,4 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
this.storageService.store(`${extensionName}-${providerId}`, sessionId, StorageScope.GLOBAL, StorageTarget.MACHINE);
addAccountUsage(this.storageService, providerId, accountName, extensionId, extensionName);
}
private getFullKey(extensionId: string): string {
return `${this.productService.urlProtocol}${extensionId}`;
}
async $getPassword(extensionId: string, key: string): Promise<string | undefined> {
const fullKey = this.getFullKey(extensionId);
const password = await this.credentialsService.getPassword(fullKey, key);
const decrypted = password && await this.encryptionService.decrypt(password);
if (decrypted) {
try {
const value = JSON.parse(decrypted);
if (value.extensionId === extensionId) {
return value.content;
}
} catch (_) {
throw new Error('Cannot get password');
}
}
return undefined;
}
async $setPassword(extensionId: string, key: string, value: string): Promise<void> {
const fullKey = this.getFullKey(extensionId);
const toEncrypt = JSON.stringify({
extensionId,
content: value
});
const encrypted = await this.encryptionService.encrypt(toEncrypt);
return this.credentialsService.setPassword(fullKey, key, encrypted);
}
async $deletePassword(extensionId: string, key: string): Promise<void> {
try {
const fullKey = this.getFullKey(extensionId);
await this.credentialsService.deletePassword(fullKey, key);
} catch (_) {
throw new Error('Cannot delete password');
}
}
}

View File

@@ -3,29 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IBulkEditService, ResourceEdit, ResourceFileEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { IExtHostContext, IWorkspaceEditDto, WorkspaceEditType, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { revive } from 'vs/base/common/marshalling';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] {
if (!data?.edits) {
return [];
}
const result: ResourceEdit[] = [];
for (let edit of revive<IWorkspaceEditDto>(data).edits) {
if (edit._type === WorkspaceEditType.File) {
result.push(new ResourceFileEdit(edit.oldUri, edit.newUri, edit.options, edit.metadata));
} else if (edit._type === WorkspaceEditType.Text) {
result.push(new ResourceTextEdit(edit.resource, edit.edit, edit.modelVersionId, edit.metadata));
} else if (edit._type === WorkspaceEditType.Cell) {
result.push(new ResourceNotebookCellEdit(edit.resource, edit.edit, edit.notebookVersionId, edit.metadata));
}
}
return result;
}
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IExtHostContext, IWorkspaceEditDto, MainThreadBulkEditsShape, MainContext } from 'vs/workbench/api/common/extHost.protocol'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors';
@extHostNamedCustomer(MainContext.MainThreadBulkEdits)
export class MainThreadBulkEdits implements MainThreadBulkEditsShape {
@@ -39,12 +19,6 @@ export class MainThreadBulkEdits implements MainThreadBulkEditsShape {
$tryApplyWorkspaceEdit(dto: IWorkspaceEditDto, undoRedoGroupId?: number): Promise<boolean> {
const edits = reviveWorkspaceEditDto2(dto);
return this._bulkEditService.apply(edits, {
// having a undoRedoGroupId means that this is a nested workspace edit,
// e.g one from a onWill-handler and for now we need to forcefully suppress
// refactor previewing, see: https://github.com/microsoft/vscode/issues/111873#issuecomment-738739852
undoRedoGroupId,
suppressPreview: typeof undoRedoGroupId === 'number' ? true : undefined
}).then(() => true, _err => false);
return this._bulkEditService.apply(edits, { undoRedoGroupId }).then(() => true, _err => false);
}
}

View File

@@ -0,0 +1,108 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Schemas } from 'vs/base/common/network';
import { isString } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CLIOutput, IExtensionGalleryService, IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionManagementCLIService } from 'vs/platform/extensionManagement/common/extensionManagementCLIService';
import { getExtensionId } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILabelService } from 'vs/platform/label/common/label';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IProductService } from 'vs/platform/product/common/productService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IExtensionManagementServerService } from 'vs/workbench/services/extensionManagement/common/extensionManagement';
import { canExecuteOnWorkspace } from 'vs/workbench/services/extensions/common/extensionsUtil';
import { IExtensionManifest } from 'vs/workbench/workbench.web.api';
// this class contains the commands that the CLI server is reying on
CommandsRegistry.registerCommand('_remoteCLI.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) {
// TODO: discuss martin, ben where to put this
const openerService = accessor.get(IOpenerService);
openerService.open(URI.revive(uri), { openExternal: true, allowTunneling: options?.allowTunneling === true });
});
interface ManageExtensionsArgs {
list?: { showVersions?: boolean, category?: string; };
install?: (string | URI)[];
uninstall?: string[];
force?: boolean;
}
CommandsRegistry.registerCommand('_remoteCLI.manageExtensions', async function (accessor: ServicesAccessor, args: ManageExtensionsArgs) {
const instantiationService = accessor.get(IInstantiationService);
const extensionManagementServerService = accessor.get(IExtensionManagementServerService);
const remoteExtensionManagementService = extensionManagementServerService.remoteExtensionManagementServer?.extensionManagementService;
if (!remoteExtensionManagementService) {
return;
}
const cliService = instantiationService.createChild(new ServiceCollection([IExtensionManagementService, remoteExtensionManagementService])).createInstance(RemoteExtensionCLIManagementService);
const lines: string[] = [];
const output = { log: lines.push.bind(lines), error: lines.push.bind(lines) };
if (args.list) {
await cliService.listExtensions(!!args.list.showVersions, args.list.category, output);
} else {
const revive = (inputs: (string | UriComponents)[]) => inputs.map(input => isString(input) ? input : URI.revive(input));
if (Array.isArray(args.install) && args.install.length) {
try {
await cliService.installExtensions(revive(args.install), [], true, !!args.force, output);
} catch (e) {
lines.push(e.message);
}
}
if (Array.isArray(args.uninstall) && args.uninstall.length) {
try {
await cliService.uninstallExtensions(revive(args.uninstall), !!args.force, output);
} catch (e) {
lines.push(e.message);
}
}
}
return lines.join('\n');
});
class RemoteExtensionCLIManagementService extends ExtensionManagementCLIService {
private _location: string | undefined;
constructor(
@IExtensionManagementService extensionManagementService: IExtensionManagementService,
@IProductService private readonly productService: IProductService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IExtensionGalleryService extensionGalleryService: IExtensionGalleryService,
@ILocalizationsService localizationsService: ILocalizationsService,
@ILabelService labelService: ILabelService,
@IWorkbenchEnvironmentService envService: IWorkbenchEnvironmentService
) {
super(extensionManagementService, extensionGalleryService, localizationsService);
const remoteAuthority = envService.remoteAuthority;
this._location = remoteAuthority ? labelService.getHostLabel(Schemas.vscodeRemote, remoteAuthority) : undefined;
}
protected get location(): string | undefined {
return this._location;
}
protected validateExtensionKind(manifest: IExtensionManifest, output: CLIOutput): boolean {
if (!canExecuteOnWorkspace(manifest, this.productService, this.configurationService)) {
output.log(localize('cannot be installed', "Cannot install '{0}' because this extension has defined that it cannot run on the remote server.", getExtensionId(manifest.publisher, manifest.name)));
return false;
}
return true;
}
}

View File

@@ -10,22 +10,18 @@ import { IRemoteConsoleLog, log } from 'vs/base/common/console';
import { logRemoteEntry } from 'vs/workbench/services/extensions/common/remoteConsoleUtil';
import { parseExtensionDevOptions } from 'vs/workbench/services/extensions/common/extensionDevOptions';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
@extHostNamedCustomer(MainContext.MainThreadConsole)
export class MainThreadConsole implements MainThreadConsoleShape {
private readonly _isExtensionDevHost: boolean;
private readonly _isExtensionDevTestFromCli: boolean;
constructor(
extHostContext: IExtHostContext,
_extHostContext: IExtHostContext,
@IEnvironmentService private readonly _environmentService: IEnvironmentService,
@ILogService private readonly _logService: ILogService,
@IExtensionHostDebugService private readonly _extensionHostDebugService: IExtensionHostDebugService,
) {
const devOpts = parseExtensionDevOptions(this._environmentService);
this._isExtensionDevHost = devOpts.isExtensionDevHost;
this._isExtensionDevTestFromCli = devOpts.isExtensionDevTestFromCli;
}
@@ -43,10 +39,5 @@ export class MainThreadConsole implements MainThreadConsoleShape {
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);
}
}
}

View File

@@ -3,15 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { multibyteAwareBtoa } from 'vs/base/browser/dom';
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, IReference } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { basename } from 'vs/base/common/path';
import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources';
import { multibyteAwareBtoa } from 'vs/base/browser/dom';
import { URI, UriComponents } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { localize } from 'vs/nls';
@@ -95,10 +95,7 @@ export class MainThreadCustomEditors extends Disposable implements extHostProtoc
dispose() {
super.dispose();
for (const disposable of this._editorProviders.values()) {
disposable.dispose();
}
dispose(this._editorProviders.values());
this._editorProviders.clear();
}

View File

@@ -327,7 +327,7 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
return {
id: sessionID,
type: session.configuration.type,
name: session.configuration.name,
name: session.name,
folderUri: session.root ? session.root.uri : undefined,
configuration: session.configuration
};

View File

@@ -0,0 +1,79 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ExtHostContext, IExtHostEditorTabsShape, IExtHostContext, MainContext, IEditorTabDto } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { Verbosity } from 'vs/workbench/common/editor';
import { GroupChangeKind, IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
export interface ITabInfo {
name: string;
resource: URI;
}
@extHostNamedCustomer(MainContext.MainThreadEditorTabs)
export class MainThreadEditorTabs {
private static _GroupEventFilter = new Set([GroupChangeKind.EDITOR_CLOSE, GroupChangeKind.EDITOR_OPEN]);
private readonly _dispoables = new DisposableStore();
private readonly _groups = new Map<IEditorGroup, IDisposable>();
private readonly _proxy: IExtHostEditorTabsShape;
constructor(
extHostContext: IExtHostContext,
@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditorTabs);
this._editorGroupsService.groups.forEach(this._subscribeToGroup, this);
this._dispoables.add(_editorGroupsService.onDidAddGroup(this._subscribeToGroup, this));
this._dispoables.add(_editorGroupsService.onDidRemoveGroup(e => {
const subscription = this._groups.get(e);
if (subscription) {
subscription.dispose();
this._groups.delete(e);
this._pushEditorTabs();
}
}));
this._pushEditorTabs();
}
dispose(): void {
dispose(this._groups.values());
this._dispoables.dispose();
}
private _subscribeToGroup(group: IEditorGroup) {
this._groups.get(group)?.dispose();
const listener = group.onDidGroupChange(e => {
if (MainThreadEditorTabs._GroupEventFilter.has(e.kind)) {
this._pushEditorTabs();
}
});
this._groups.set(group, listener);
}
private _pushEditorTabs(): void {
const tabs: IEditorTabDto[] = [];
for (const group of this._editorGroupsService.groups) {
for (const editor of group.editors) {
if (editor.isDisposed() || !editor.resource) {
continue;
}
tabs.push({
group: group.id,
name: editor.getTitle(Verbosity.SHORT) ?? '',
resource: editor.resource
});
}
}
this._proxy.$acceptEditorTabs(tabs);
}
}

View File

@@ -27,7 +27,7 @@ import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/wo
import { revive } from 'vs/base/common/marshalling';
import { ResourceNotebookCellEdit } from 'vs/workbench/contrib/bulkEdit/browser/bulkCellEdits';
function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] {
export function reviveWorkspaceEditDto2(data: IWorkspaceEditDto | undefined): ResourceEdit[] {
if (!data?.edits) {
return [];
}

View File

@@ -7,7 +7,7 @@ import { SerializedError } from 'vs/base/common/errors';
import Severity from 'vs/base/common/severity';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IExtHostContext, MainContext, MainThreadExtensionServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { IExtensionService, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions';
import { IExtensionService, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { localize } from 'vs/nls';
@@ -19,29 +19,23 @@ import { IExtensionsWorkbenchService } from 'vs/workbench/contrib/extensions/com
import { CancellationToken } from 'vs/base/common/cancellation';
import { ILocalExtension } from 'vs/platform/extensionManagement/common/extensionManagement';
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
import { ITimerService } from 'vs/workbench/services/timer/browser/timerService';
@extHostNamedCustomer(MainContext.MainThreadExtensionService)
export class MainThreadExtensionService implements MainThreadExtensionServiceShape {
private readonly _extensionService: IExtensionService;
private readonly _notificationService: INotificationService;
private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService;
private readonly _hostService: IHostService;
private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService;
private readonly _extensionHostKind: ExtensionHostKind;
constructor(
extHostContext: IExtHostContext,
@IExtensionService extensionService: IExtensionService,
@INotificationService notificationService: INotificationService,
@IExtensionsWorkbenchService extensionsWorkbenchService: IExtensionsWorkbenchService,
@IHostService hostService: IHostService,
@IWorkbenchExtensionEnablementService extensionEnablementService: IWorkbenchExtensionEnablementService
@IExtensionService private readonly _extensionService: IExtensionService,
@INotificationService private readonly _notificationService: INotificationService,
@IExtensionsWorkbenchService private readonly _extensionsWorkbenchService: IExtensionsWorkbenchService,
@IHostService private readonly _hostService: IHostService,
@IWorkbenchExtensionEnablementService private readonly _extensionEnablementService: IWorkbenchExtensionEnablementService,
@ITimerService private readonly _timerService: ITimerService,
) {
this._extensionService = extensionService;
this._notificationService = notificationService;
this._extensionsWorkbenchService = extensionsWorkbenchService;
this._hostService = hostService;
this._extensionEnablementService = extensionEnablementService;
this._extensionHostKind = extHostContext.extensionHostKind;
}
public dispose(): void {
@@ -131,4 +125,14 @@ export class MainThreadExtensionService implements MainThreadExtensionServiceSha
async $onExtensionHostExit(code: number): Promise<void> {
this._extensionService._onExtensionHostExit(code);
}
async $setPerformanceMarks(marks: PerformanceMark[]): Promise<void> {
if (this._extensionHostKind === ExtensionHostKind.LocalProcess) {
this._timerService.setPerformanceMarks('localExtHost', marks);
} else if (this._extensionHostKind === ExtensionHostKind.LocalWebWorker) {
this._timerService.setPerformanceMarks('workerExtHost', marks);
} else {
this._timerService.setPerformanceMarks('remoteExtHost', marks);
}
}
}

View File

@@ -4,23 +4,43 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from 'vs/base/common/lifecycle';
import { FileChangeType, IFileService } from 'vs/platform/files/common/files';
import { FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files';
import { extHostCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { ExtHostContext, FileSystemEvents, IExtHostContext } from '../common/extHost.protocol';
import { localize } from 'vs/nls';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { IWorkingCopyFileOperationParticipant, IWorkingCopyFileService, SourceTargetPair, IFileOperationUndoRedoInfo } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { reviveWorkspaceEditDto2 } from 'vs/workbench/api/browser/mainThreadEditors';
import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService';
import { IProgressService, ProgressLocation } from 'vs/platform/progress/common/progress';
import { raceCancellation } from 'vs/base/common/async';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import Severity from 'vs/base/common/severity';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@extHostCustomer
export class MainThreadFileSystemEventService {
static readonly MementoKeyAdditionalEdits = `file.particpants.additionalEdits`;
private readonly _listener = new DisposableStore();
constructor(
extHostContext: IExtHostContext,
@IFileService fileService: IFileService,
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService
@IWorkingCopyFileService workingCopyFileService: IWorkingCopyFileService,
@IBulkEditService bulkEditService: IBulkEditService,
@IProgressService progressService: IProgressService,
@IDialogService dialogService: IDialogService,
@IStorageService storageService: IStorageService,
@ILogService logService: ILogService,
@IEnvironmentService envService: IEnvironmentService
) {
const proxy = extHostContext.getProxy(ExtHostContext.ExtHostFileSystemEventService);
@@ -53,14 +73,124 @@ export class MainThreadFileSystemEventService {
}));
// BEFORE file operation
this._listener.add(workingCopyFileService.addFileOperationParticipant({
participate: async (files, operation, undoRedoGroupId, isUndoing, _progress, timeout, token) => {
if (!isUndoing) {
return proxy.$onWillRunFileOperation(operation, files, undoRedoGroupId, timeout, token);
const fileOperationParticipant = new class implements IWorkingCopyFileOperationParticipant {
async participate(files: SourceTargetPair[], operation: FileOperation, undoInfo: IFileOperationUndoRedoInfo | undefined, timeout: number, token: CancellationToken) {
if (undoInfo?.isUndoing) {
return;
}
const cts = new CancellationTokenSource(token);
const timer = setTimeout(() => cts.cancel(), timeout);
const data = await progressService.withProgress({
location: ProgressLocation.Notification,
title: this._progressLabel(operation),
cancellable: true,
delay: Math.min(timeout / 2, 3000)
}, () => {
// race extension host event delivery against timeout AND user-cancel
const onWillEvent = proxy.$onWillRunFileOperation(operation, files, timeout, token);
return raceCancellation(onWillEvent, cts.token);
}, () => {
// user-cancel
cts.cancel();
}).finally(() => {
cts.dispose();
clearTimeout(timer);
});
if (!data) {
// cancelled or no reply
return;
}
const needsConfirmation = data.edit.edits.some(edit => edit.metadata?.needsConfirmation);
let showPreview = storageService.getBoolean(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL);
if (envService.extensionTestsLocationURI) {
// don't show dialog in tests
showPreview = false;
}
if (showPreview === undefined) {
// show a user facing message
let message: string;
if (data.extensionNames.length === 1) {
if (operation === FileOperation.CREATE) {
message = localize('ask.1.create', "Extension '{0}' wants to make refactoring changes with this file creation", data.extensionNames[0]);
} else if (operation === FileOperation.COPY) {
message = localize('ask.1.copy', "Extension '{0}' wants to make refactoring changes with this file copy", data.extensionNames[0]);
} else if (operation === FileOperation.MOVE) {
message = localize('ask.1.move', "Extension '{0}' wants to make refactoring changes with this file move", data.extensionNames[0]);
} else /* if (operation === FileOperation.DELETE) */ {
message = localize('ask.1.delete', "Extension '{0}' wants to make refactoring changes with this file deletion", data.extensionNames[0]);
}
} else {
if (operation === FileOperation.CREATE) {
message = localize('ask.N.create', "{0} extensions want to make refactoring changes with this file creation", data.extensionNames.length);
} else if (operation === FileOperation.COPY) {
message = localize('ask.N.copy', "{0} extensions want to make refactoring changes with this file copy", data.extensionNames.length);
} else if (operation === FileOperation.MOVE) {
message = localize('ask.N.move', "{0} extensions want to make refactoring changes with this file move", data.extensionNames.length);
} else /* if (operation === FileOperation.DELETE) */ {
message = localize('ask.N.delete', "{0} extensions want to make refactoring changes with this file deletion", data.extensionNames.length);
}
}
if (needsConfirmation) {
// edit which needs confirmation -> always show dialog
const answer = await dialogService.show(Severity.Info, message, [localize('preview', "Show Preview"), localize('cancel', "Skip Changes")], { cancelId: 1 });
showPreview = true;
if (answer.choice === 1) {
// no changes wanted
return;
}
} else {
// choice
const answer = await dialogService.show(Severity.Info, message,
[localize('ok', "OK"), localize('preview', "Show Preview"), localize('cancel', "Skip Changes")],
{
cancelId: 2,
checkbox: { label: localize('again', "Don't ask again") }
}
);
if (answer.choice === 2) {
// no changes wanted, don't persist cancel option
return;
}
showPreview = answer.choice === 1;
if (answer.checkboxChecked /* && answer.choice !== 2 */) {
storageService.store(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, showPreview, StorageScope.GLOBAL, StorageTarget.USER);
}
}
}
logService.info('[onWill-handler] applying additional workspace edit from extensions', data.extensionNames);
await bulkEditService.apply(
reviveWorkspaceEditDto2(data.edit),
{ undoRedoGroupId: undoInfo?.undoRedoGroupId, showPreview }
);
}
private _progressLabel(operation: FileOperation): string {
switch (operation) {
case FileOperation.CREATE:
return localize('msg-create', "Running 'File Create' participants...");
case FileOperation.MOVE:
return localize('msg-rename', "Running 'File Rename' participants...");
case FileOperation.COPY:
return localize('msg-copy', "Running 'File Copy' participants...");
case FileOperation.DELETE:
return localize('msg-delete', "Running 'File Delete' participants...");
}
}
}));
};
// BEFORE file operation
this._listener.add(workingCopyFileService.addFileOperationParticipant(fileOperationParticipant));
// AFTER file operation
this._listener.add(workingCopyFileService.onDidRunWorkingCopyFileOperation(e => proxy.$onDidRunFileOperation(e.operation, e.files)));
@@ -71,6 +201,19 @@ export class MainThreadFileSystemEventService {
}
}
registerAction2(class ResetMemento extends Action2 {
constructor() {
super({
id: 'files.participants.resetChoice',
title: localize('label', "Reset choice for 'File operation needs preview'"),
f1: true
});
}
run(accessor: ServicesAccessor) {
accessor.get(IStorageService).remove(MainThreadFileSystemEventService.MementoKeyAdditionalEdits, StorageScope.GLOBAL);
}
});
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
id: 'files',

View File

@@ -21,7 +21,7 @@ import { Selection } from 'vs/editor/common/core/selection';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { mixin } from 'vs/base/common/objects';
import { decodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto';
import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto';
@extHostNamedCustomer(MainContext.MainThreadLanguageFeatures)
export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesShape {
@@ -497,6 +497,32 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
}));
}
// --- inline hints
$registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void {
const provider = <modes.InlineHintsProvider>{
provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise<modes.InlineHint[] | undefined> => {
const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token);
return result?.hints;
}
};
if (typeof eventHandle === 'number') {
const emitter = new Emitter<void>();
this._registrations.set(eventHandle, emitter);
provider.onDidChangeInlineHints = emitter.event;
}
this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider));
}
$emitInlineHintsEvent(eventHandle: number, event?: any): void {
const obj = this._registrations.get(eventHandle);
if (obj instanceof Emitter) {
obj.fire(event);
}
}
// --- links
$registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void {
@@ -662,7 +688,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha
return {
beforeText: MainThreadLanguageFeatures._reviveRegExp(onEnterRule.beforeText),
afterText: onEnterRule.afterText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.afterText) : undefined,
oneLineAboveText: onEnterRule.oneLineAboveText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.oneLineAboveText) : undefined,
previousLineText: onEnterRule.previousLineText ? MainThreadLanguageFeatures._reviveRegExp(onEnterRule.previousLineText) : undefined,
action: onEnterRule.action
};
}

View File

@@ -33,7 +33,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape {
$showMessage(severity: Severity, message: string, options: MainThreadMessageOptions, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise<number | undefined> {
if (options.modal) {
return this._showModalMessage(severity, message, commands);
return this._showModalMessage(severity, message, commands, options.useCustom);
} else {
return this._showMessage(severity, message, commands, options.extension);
}
@@ -97,7 +97,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape {
});
}
private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[]): Promise<number | undefined> {
private async _showModalMessage(severity: Severity, message: string, commands: { title: string; isCloseAffordance: boolean; handle: number; }[], useCustom?: boolean): Promise<number | undefined> {
let cancelId: number | undefined = undefined;
const buttons = commands.map((command, index) => {
@@ -118,7 +118,7 @@ export class MainThreadMessageService implements MainThreadMessageServiceShape {
cancelId = buttons.length - 1;
}
const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId });
const { choice } = await this._dialogService.show(severity, message, buttons, { cancelId, useCustom });
return choice === commands.length ? undefined : commands[choice].handle;
}
}

View File

@@ -16,8 +16,8 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { EditorActivation, ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { viewColumnToEditorGroup } from 'vs/workbench/common/editor';
import { INotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
@@ -129,12 +129,12 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@ILogService private readonly logService: ILogService,
@INotebookCellStatusBarService private readonly cellStatusBarService: INotebookCellStatusBarService,
@IWorkingCopyService private readonly _workingCopyService: IWorkingCopyService,
@INotebookEditorModelResolverService private readonly _notebookModelResolverService: INotebookEditorModelResolverService,
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostNotebook);
@@ -646,14 +646,13 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
switch (revealType) {
case NotebookEditorRevealType.Default:
notebookEditor.revealInView(cell);
break;
return notebookEditor.revealCellRangeInView(range);
case NotebookEditorRevealType.InCenter:
notebookEditor.revealInCenter(cell);
break;
return notebookEditor.revealInCenter(cell);
case NotebookEditorRevealType.InCenterIfOutsideViewport:
notebookEditor.revealInCenterIfOutsideViewport(cell);
break;
return notebookEditor.revealInCenterIfOutsideViewport(cell);
case NotebookEditorRevealType.AtTop:
return notebookEditor.revealInViewAtTop(cell);
default:
break;
}
@@ -733,7 +732,7 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
const input = this.editorService.createEditorInput({ resource: URI.revive(resource), options: editorOptions });
// TODO: handle options.selection
const editorPane = await openEditorWith(input, viewType, options, group, this.editorService, this.configurationService, this.quickInputService);
const editorPane = await this._instantiationService.invokeFunction(openEditorWith, input, viewType, options, group);
const notebookEditor = (editorPane as unknown as { isNotebookEditor?: boolean })?.isNotebookEditor ? (editorPane!.getControl() as INotebookEditor) : undefined;
if (notebookEditor) {

View File

@@ -0,0 +1,73 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { IProductService } from 'vs/platform/product/common/productService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { ICredentialsService } from 'vs/workbench/services/credentials/common/credentials';
import { IEncryptionService } from 'vs/workbench/services/encryption/common/encryptionService';
import { ExtHostContext, ExtHostSecretStateShape, IExtHostContext, MainContext, MainThreadSecretStateShape } from '../common/extHost.protocol';
@extHostNamedCustomer(MainContext.MainThreadSecretState)
export class MainThreadSecretState extends Disposable implements MainThreadSecretStateShape {
private readonly _proxy: ExtHostSecretStateShape;
constructor(
extHostContext: IExtHostContext,
@ICredentialsService private readonly credentialsService: ICredentialsService,
@IEncryptionService private readonly encryptionService: IEncryptionService,
@IProductService private readonly productService: IProductService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSecretState);
this._register(this.credentialsService.onDidChangePassword(e => {
const extensionId = e.service.substring(this.productService.urlProtocol.length);
this._proxy.$onDidChangePassword({ extensionId, key: e.account });
}));
}
private getFullKey(extensionId: string): string {
return `${this.productService.urlProtocol}${extensionId}`;
}
async $getPassword(extensionId: string, key: string): Promise<string | undefined> {
const fullKey = this.getFullKey(extensionId);
const password = await this.credentialsService.getPassword(fullKey, key);
const decrypted = password && await this.encryptionService.decrypt(password);
if (decrypted) {
try {
const value = JSON.parse(decrypted);
if (value.extensionId === extensionId) {
return value.content;
}
} catch (_) {
throw new Error('Cannot get password');
}
}
return undefined;
}
async $setPassword(extensionId: string, key: string, value: string): Promise<void> {
const fullKey = this.getFullKey(extensionId);
const toEncrypt = JSON.stringify({
extensionId,
content: value
});
const encrypted = await this.encryptionService.encrypt(toEncrypt);
return this.credentialsService.setPassword(fullKey, key, encrypted);
}
async $deletePassword(extensionId: string, key: string): Promise<void> {
try {
const fullKey = this.getFullKey(extensionId);
await this.credentialsService.deletePassword(fullKey, key);
} catch (_) {
throw new Error('Cannot delete password');
}
}
}

View File

@@ -567,13 +567,19 @@ export class MainThreadTask implements MainThreadTaskShape {
if (!task) {
reject(new Error('Task not found'));
} else {
this._taskService.run(task).then(undefined, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
const result: TaskExecutionDTO = {
id: value.id,
task: TaskDTO.from(task)
};
this._taskService.run(task).then(summary => {
// Ensure that the task execution gets cleaned up if the exit code is undefined
// This can happen when the task has dependent tasks and one of them failed
if ((summary?.exitCode === undefined) || (summary.exitCode !== 0)) {
this._proxy.$OnDidEndTask(result);
}
}, reason => {
// eat the error, it has already been surfaced to the user and we don't care about it here
});
resolve(result);
}
}, (_error) => {

View File

@@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, EXT_HOST_CREATION_DELAY, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest } from 'vs/workbench/contrib/terminal/common/terminal';
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto } from 'vs/workbench/api/common/extHost.protocol';
import { IShellLaunchConfig, ITerminalProcessExtHostProxy, ISpawnExtHostProcessRequest, ITerminalDimensions, IAvailableShellsRequest, IDefaultShellAndArgsRequest, IStartExtensionTerminalRequest, ITerminalConfiguration, TERMINAL_CONFIG_SECTION } from 'vs/workbench/contrib/terminal/common/terminal';
import { ExtHostContext, ExtHostTerminalServiceShape, MainThreadTerminalServiceShape, MainContext, IExtHostContext, IShellLaunchConfigDto, TerminalLaunchConfig, ITerminalDimensionsDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { URI } from 'vs/base/common/uri';
import { StopWatch } from 'vs/base/common/stopwatch';
@@ -16,11 +16,18 @@ import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/termi
import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { ILogService } from 'vs/platform/log/common/log';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
private _proxy: ExtHostTerminalServiceShape;
/**
* Stores a map from a temporary terminal id (a UUID generated on the extension host side)
* to a numeric terminal id (an id generated on the renderer side)
* This comes in play only when dealing with terminals created on the extension host side
*/
private _extHostTerminalIds = new Map<string, number>();
private _remoteAuthority: string | null;
private readonly _toDispose = new DisposableStore();
private readonly _terminalProcessProxies = new Map<number, ITerminalProcessExtHostProxy>();
@@ -40,6 +47,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ILogService private readonly _logService: ILogService,
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
@@ -47,13 +55,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
// ITerminalService listeners
this._toDispose.add(_terminalService.onInstanceCreated((instance) => {
// Delay this message so the TerminalInstance constructor has a chance to finish and
// return the ID normally to the extension host. The ID that is passed here will be
// used to register non-extension API terminals in the extension host.
setTimeout(() => {
this._onTerminalOpened(instance);
this._onInstanceDimensionsChanged(instance);
}, EXT_HOST_CREATION_DELAY);
this._onTerminalOpened(instance);
this._onInstanceDimensionsChanged(instance);
}));
this._toDispose.add(_terminalService.onInstanceDisposed(instance => this._onTerminalDisposed(instance)));
@@ -100,7 +103,22 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
// when the extension host process goes down ?
}
public $createTerminal(launchConfig: TerminalLaunchConfig): Promise<{ id: number, name: string }> {
private _getTerminalId(id: TerminalIdentifier): number | undefined {
if (typeof id === 'number') {
return id;
}
return this._extHostTerminalIds.get(id);
}
private _getTerminalInstance(id: TerminalIdentifier): ITerminalInstance | undefined {
const rendererId = this._getTerminalId(id);
if (typeof rendererId === 'number') {
return this._terminalService.getInstanceFromId(rendererId);
}
return undefined;
}
public async $createTerminal(extHostTerminalId: string, launchConfig: TerminalLaunchConfig): Promise<void> {
const shellLaunchConfig: IShellLaunchConfig = {
name: launchConfig.name,
executable: launchConfig.shellPath,
@@ -112,39 +130,38 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
strictEnv: launchConfig.strictEnv,
hideFromUser: launchConfig.hideFromUser,
isExtensionTerminal: launchConfig.isExtensionTerminal,
extHostTerminalId: extHostTerminalId,
isFeatureTerminal: launchConfig.isFeatureTerminal
};
const terminal = this._terminalService.createTerminal(shellLaunchConfig);
return Promise.resolve({
id: terminal.id,
name: terminal.title
});
this._extHostTerminalIds.set(extHostTerminalId, terminal.id);
}
public $show(terminalId: number, preserveFocus: boolean): void {
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
public $show(id: TerminalIdentifier, preserveFocus: boolean): void {
const terminalInstance = this._getTerminalInstance(id);
if (terminalInstance) {
this._terminalService.setActiveInstance(terminalInstance);
this._terminalService.showPanel(!preserveFocus);
}
}
public $hide(terminalId: number): void {
public $hide(id: TerminalIdentifier): void {
const rendererId = this._getTerminalId(id);
const instance = this._terminalService.getActiveInstance();
if (instance && instance.id === terminalId) {
if (instance && instance.id === rendererId) {
this._terminalService.hidePanel();
}
}
public $dispose(terminalId: number): void {
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
public $dispose(id: TerminalIdentifier): void {
const terminalInstance = this._getTerminalInstance(id);
if (terminalInstance) {
terminalInstance.dispose();
}
}
public $sendText(terminalId: number, text: string, addNewLine: boolean): void {
const terminalInstance = this._terminalService.getInstanceFromId(terminalId);
public $sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void {
const terminalInstance = this._getTerminalInstance(id);
if (terminalInstance) {
terminalInstance.sendText(text, addNewLine);
}
@@ -204,6 +221,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
private _onTerminalOpened(terminalInstance: ITerminalInstance): void {
const extHostTerminalId = terminalInstance.shellLaunchConfig.extHostTerminalId;
const shellLaunchConfigDto: IShellLaunchConfigDto = {
name: terminalInstance.shellLaunchConfig.name,
executable: terminalInstance.shellLaunchConfig.executable,
@@ -212,13 +230,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
env: terminalInstance.shellLaunchConfig.env,
hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser
};
if (terminalInstance.title) {
this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto);
} else {
terminalInstance.waitForTitle().then(title => {
this._proxy.$acceptTerminalOpened(terminalInstance.id, title, shellLaunchConfigDto);
});
}
this._proxy.$acceptTerminalOpened(terminalInstance.id, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto);
}
private _onTerminalProcessIdReady(terminalInstance: ITerminalInstance): void {
@@ -249,7 +261,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
executable: request.shellLaunchConfig.executable,
args: request.shellLaunchConfig.args,
cwd: request.shellLaunchConfig.cwd,
env: request.shellLaunchConfig.env
env: request.shellLaunchConfig.env,
flowControl: this._configurationService.getValue<ITerminalConfiguration>(TERMINAL_CONFIG_SECTION).flowControl
};
this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request });
@@ -260,8 +273,9 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
request.cols,
request.rows,
request.isWorkspaceShellAllowed
).then(request.callback);
).then(request.callback, request.callback);
proxy.onAcknowledgeDataEvent(charCount => this._proxy.$acceptProcessAckDataEvent(proxy.terminalId, charCount));
proxy.onInput(data => this._proxy.$acceptProcessInput(proxy.terminalId, data));
proxy.onResize(dimensions => this._proxy.$acceptProcessResize(proxy.terminalId, dimensions.cols, dimensions.rows));
proxy.onShutdown(immediate => this._proxy.$acceptProcessShutdown(proxy.terminalId, immediate));
@@ -294,38 +308,59 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
public $sendProcessTitle(terminalId: number, title: string): void {
this._getTerminalProcess(terminalId).emitTitle(title);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitTitle(title);
}
}
public $sendProcessData(terminalId: number, data: string): void {
this._getTerminalProcess(terminalId).emitData(data);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitData(data);
}
}
public $sendProcessReady(terminalId: number, pid: number, cwd: string): void {
this._getTerminalProcess(terminalId).emitReady(pid, cwd);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitReady(pid, cwd);
}
}
public $sendProcessExit(terminalId: number, exitCode: number | undefined): void {
this._getTerminalProcess(terminalId).emitExit(exitCode);
this._terminalProcessProxies.delete(terminalId);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitExit(exitCode);
this._terminalProcessProxies.delete(terminalId);
}
}
public $sendOverrideDimensions(terminalId: number, dimensions: ITerminalDimensions | undefined): void {
this._getTerminalProcess(terminalId).emitOverrideDimensions(dimensions);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitOverrideDimensions(dimensions);
}
}
public $sendProcessInitialCwd(terminalId: number, initialCwd: string): void {
this._getTerminalProcess(terminalId).emitInitialCwd(initialCwd);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitInitialCwd(initialCwd);
}
}
public $sendProcessCwd(terminalId: number, cwd: string): void {
this._getTerminalProcess(terminalId).emitCwd(cwd);
const terminalProcess = this._terminalProcessProxies.get(terminalId);
if (terminalProcess) {
terminalProcess.emitCwd(cwd);
}
}
public $sendResolvedLaunchConfig(terminalId: number, shellLaunchConfig: IShellLaunchConfig): void {
const instance = this._terminalService.getInstanceFromId(terminalId);
if (instance) {
this._getTerminalProcess(terminalId).emitResolvedShellLaunchConfig(shellLaunchConfig);
this._getTerminalProcess(terminalId)?.emitResolvedShellLaunchConfig(shellLaunchConfig);
}
}
@@ -338,7 +373,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
sw.stop();
sum += sw.elapsed();
}
this._getTerminalProcess(terminalId).emitLatency(sum / COUNT);
this._getTerminalProcess(terminalId)?.emitLatency(sum / COUNT);
}
private _isPrimaryExtHost(): boolean {
@@ -363,10 +398,11 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
}
}
private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy {
private _getTerminalProcess(terminalId: number): ITerminalProcessExtHostProxy | undefined {
const terminal = this._terminalProcessProxies.get(terminalId);
if (!terminal) {
throw new Error(`Unknown terminal: ${terminalId}`);
this._logService.error(`Unknown terminal: ${terminalId}`);
return undefined;
}
return terminal;
}

View File

@@ -3,12 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { URI, UriComponents } from 'vs/base/common/uri';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { getTestSubscriptionKey, RunTestsRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { ITestResultService } from 'vs/workbench/contrib/testing/common/testResultService';
import { ITestService } from 'vs/workbench/contrib/testing/common/testService';
import { ExtHostContext, ExtHostTestingResource, ExtHostTestingShape, IExtHostContext, MainContext, MainThreadTestingShape } from '../common/extHost.protocol';
const reviveDiff = (diff: TestsDiff) => {
for (const entry of diff) {
if (entry[0] === TestDiffOpType.Add || entry[0] === TestDiffOpType.Update) {
const item = entry[1];
if (item.item.location) {
item.item.location.uri = URI.revive(item.item.location.uri);
}
for (const message of item.item.state.messages) {
if (message.location) {
message.location.uri = URI.revive(message.location.uri);
}
}
}
}
};
@extHostNamedCustomer(MainContext.MainThreadTesting)
export class MainThreadTesting extends Disposable implements MainThreadTestingShape {
@@ -18,11 +37,28 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
constructor(
extHostContext: IExtHostContext,
@ITestService private readonly testService: ITestService,
@ITestResultService resultService: ITestResultService,
) {
super();
this.proxy = extHostContext.getProxy(ExtHostContext.ExtHostTesting);
this._register(this.testService.onShouldSubscribe(args => this.proxy.$subscribeToTests(args.resource, args.uri)));
this._register(this.testService.onShouldUnsubscribe(args => this.proxy.$unsubscribeFromTests(args.resource, args.uri)));
const testCompleteListener = this._register(new MutableDisposable());
this._register(resultService.onNewTestResult(results => {
testCompleteListener.value = results.onComplete(() => this.proxy.$publishTestResults({ tests: results.tests }));
}));
testService.updateRootProviderCount(1);
const lastCompleted = resultService.results.find(r => !r.isComplete);
if (lastCompleted) {
this.proxy.$publishTestResults({ tests: lastCompleted.tests });
}
for (const { resource, uri } of this.testService.subscriptions) {
this.proxy.$subscribeToTests(resource, uri);
}
}
/**
@@ -30,7 +66,8 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
*/
public $registerTestProvider(id: string) {
this.testService.registerTestController(id, {
runTests: req => this.proxy.$runTestsForProvider(req),
runTests: (req, token) => this.proxy.$runTestsForProvider(req, token),
lookupTest: test => this.proxy.$lookupTest(test),
});
}
@@ -64,14 +101,19 @@ export class MainThreadTesting extends Disposable implements MainThreadTestingSh
* @inheritdoc
*/
public $publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void {
reviveDiff(diff);
this.testService.publishDiff(resource, URI.revive(uri), diff);
}
public $runTests(req: RunTestsRequest): Promise<RunTestsResult> {
return this.testService.runTests(req);
public $runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult> {
return this.testService.runTests(req, token);
}
public dispose() {
// no-op
this.testService.updateRootProviderCount(-1);
for (const subscription of this.testSubscriptions.values()) {
subscription.dispose();
}
this.testSubscriptions.clear();
}
}

View File

@@ -222,8 +222,8 @@ class TreeViewDataProvider implements ITreeViewDataProvider {
const hasResolve = await this.hasResolve;
if (elements) {
for (const element of elements) {
const resolvable = new ResolvableTreeItem(element, hasResolve ? () => {
return this._proxy.$resolve(this.treeViewId, element.handle);
const resolvable = new ResolvableTreeItem(element, hasResolve ? (token) => {
return this._proxy.$resolve(this.treeViewId, element.handle, token);
} : undefined);
this.itemsMap.set(element.handle, resolvable);
result.push(resolvable);

View File

@@ -3,22 +3,31 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { MainThreadTunnelServiceShape, IExtHostContext, MainContext, ExtHostContext, ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { CandidatePort, IRemoteExplorerService, makeAddress } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ITunnelProvider, ITunnelService, TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions, RemoteTunnel, isPortPrivileged } from 'vs/platform/remote/common/tunnel';
import { Disposable } from 'vs/base/common/lifecycle';
import type { TunnelDescription } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { PORT_AUTO_FORWARD_SETTING } from 'vs/workbench/contrib/remote/browser/tunnelView';
import { ILogService } from 'vs/platform/log/common/log';
@extHostNamedCustomer(MainContext.MainThreadTunnelService)
export class MainThreadTunnelService extends Disposable implements MainThreadTunnelServiceShape {
private readonly _proxy: ExtHostTunnelServiceShape;
private elevateionRetry: boolean = false;
constructor(
extHostContext: IExtHostContext,
@IRemoteExplorerService private readonly remoteExplorerService: IRemoteExplorerService,
@ITunnelService private readonly tunnelService: ITunnelService
@ITunnelService private readonly tunnelService: ITunnelService,
@INotificationService private readonly notificationService: INotificationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@ILogService private readonly logService: ILogService
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTunnelService);
@@ -26,14 +35,50 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
this._register(tunnelService.onTunnelClosed(() => this._proxy.$onDidTunnelsChange()));
}
async $setCandidateFinder(): Promise<void> {
if (this.remoteExplorerService.portsFeaturesEnabled) {
this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING));
} else {
this._register(this.remoteExplorerService.onEnabledPortsFeatures(() => this._proxy.$registerCandidateFinder(this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING))));
}
this._register(this.configurationService.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration(PORT_AUTO_FORWARD_SETTING)) {
return this._proxy.$registerCandidateFinder((this.configurationService.getValue(PORT_AUTO_FORWARD_SETTING)));
}
}));
}
async $openTunnel(tunnelOptions: TunnelOptions, source: string): Promise<TunnelDto | undefined> {
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source);
const tunnel = await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, false);
if (tunnel) {
if (!this.elevateionRetry
&& (tunnelOptions.localAddressPort !== undefined)
&& (tunnel.tunnelLocalPort !== undefined)
&& isPortPrivileged(tunnelOptions.localAddressPort)
&& (tunnel.tunnelLocalPort !== tunnelOptions.localAddressPort)
&& this.tunnelService.canElevate) {
this.elevationPrompt(tunnelOptions, tunnel, source);
}
return TunnelDto.fromServiceTunnel(tunnel);
}
return undefined;
}
private async elevationPrompt(tunnelOptions: TunnelOptions, tunnel: RemoteTunnel, source: string) {
return this.notificationService.prompt(Severity.Info,
nls.localize('remote.tunnel.openTunnel', "The extension {0} has forwarded port {1}. You'll need to run as superuser to use port {2} locally.", source, tunnelOptions.remoteAddress.port, tunnelOptions.localAddressPort),
[{
label: nls.localize('remote.tunnelsView.elevationButton', "Use Port {0} as Sudo...", tunnel.tunnelRemotePort),
run: async () => {
this.elevateionRetry = true;
await this.remoteExplorerService.close({ host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort });
await this.remoteExplorerService.forward(tunnelOptions.remoteAddress, tunnelOptions.localAddressPort, tunnelOptions.label, source, true);
this.elevateionRetry = false;
}
}]);
}
async $closeTunnel(remote: { host: string, port: number }): Promise<void> {
return this.remoteExplorerService.close(remote);
}
@@ -47,27 +92,29 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
});
}
async $onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise<void> {
async $onFoundNewCandidates(candidates: CandidatePort[]): Promise<void> {
this.remoteExplorerService.onFoundNewCandidates(candidates);
}
async $tunnelServiceReady(): Promise<void> {
return this.remoteExplorerService.restore();
}
async $setTunnelProvider(): Promise<void> {
async $setTunnelProvider(features: TunnelProviderFeatures): Promise<void> {
const tunnelProvider: ITunnelProvider = {
forwardPort: (tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions) => {
const forward = this._proxy.$forwardPort(tunnelOptions, tunnelCreationOptions);
if (forward) {
return forward.then(tunnel => {
this.logService.trace(`MainThreadTunnelService: New tunnel established by tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`);
if (!tunnel) {
return undefined;
}
return {
tunnelRemotePort: tunnel.remoteAddress.port,
tunnelRemoteHost: tunnel.remoteAddress.host,
localAddress: typeof tunnel.localAddress === 'string' ? tunnel.localAddress : makeAddress(tunnel.localAddress.host, tunnel.localAddress.port),
tunnelLocalPort: typeof tunnel.localAddress !== 'string' ? tunnel.localAddress.port : undefined,
dispose: (silent?: boolean) => {
this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent);
public: tunnel.public,
dispose: async (silent?: boolean) => {
this.logService.trace(`MainThreadTunnelService: Closing tunnel from tunnel provider: ${tunnel?.remoteAddress.host}:${tunnel?.remoteAddress.port}`);
return this._proxy.$closeTunnel({ host: tunnel.remoteAddress.host, port: tunnel.remoteAddress.port }, silent);
}
};
});
@@ -75,9 +122,16 @@ export class MainThreadTunnelService extends Disposable implements MainThreadTun
return undefined;
}
};
this.tunnelService.setTunnelProvider(tunnelProvider);
this.tunnelService.setTunnelProvider(tunnelProvider, features);
}
async $setCandidateFilter(): Promise<void> {
this.remoteExplorerService.setCandidateFilter((candidates: CandidatePort[]): Promise<CandidatePort[]> => {
return this._proxy.$applyCandidateFilter(candidates);
});
}
dispose(): void {
}

View File

@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ExtHostContext, ExtHostUriOpenersShape, IExtHostContext, MainContext, MainThreadUriOpenersShape } from 'vs/workbench/api/common/extHost.protocol';
import { defaultExternalUriOpenerId } from 'vs/workbench/contrib/externalUriOpener/common/configuration';
import { ContributedExternalUriOpenersStore } from 'vs/workbench/contrib/externalUriOpener/common/contributedOpeners';
import { IExternalOpenerProvider, IExternalUriOpener, IExternalUriOpenerService } from 'vs/workbench/contrib/externalUriOpener/common/externalUriOpenerService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { extHostNamedCustomer } from '../common/extHostCustomers';
interface RegisteredOpenerMetadata {
readonly schemes: ReadonlySet<string>;
readonly extensionId: ExtensionIdentifier;
readonly label: string;
}
@extHostNamedCustomer(MainContext.MainThreadUriOpeners)
export class MainThreadUriOpeners extends Disposable implements MainThreadUriOpenersShape, IExternalOpenerProvider {
private readonly proxy: ExtHostUriOpenersShape;
private readonly _registeredOpeners = new Map<string, RegisteredOpenerMetadata>();
private readonly _contributedExternalUriOpenersStore: ContributedExternalUriOpenersStore;
constructor(
context: IExtHostContext,
@IStorageService storageService: IStorageService,
@IExternalUriOpenerService externalUriOpenerService: IExternalUriOpenerService,
@IExtensionService private readonly extensionService: IExtensionService,
@IOpenerService private readonly openerService: IOpenerService,
@INotificationService private readonly notificationService: INotificationService,
) {
super();
this.proxy = context.getProxy(ExtHostContext.ExtHostUriOpeners);
this._register(externalUriOpenerService.registerExternalOpenerProvider(this));
this._contributedExternalUriOpenersStore = this._register(new ContributedExternalUriOpenersStore(storageService, extensionService));
}
public async *getOpeners(targetUri: URI): AsyncIterable<IExternalUriOpener> {
// Currently we only allow openers for http and https urls
if (targetUri.scheme !== Schemas.http && targetUri.scheme !== Schemas.https) {
return;
}
await this.extensionService.activateByEvent(`onUriOpen:${targetUri.scheme}`);
for (const [id, openerMetadata] of this._registeredOpeners) {
if (openerMetadata.schemes.has(targetUri.scheme)) {
yield this.createOpener(id, openerMetadata);
}
}
}
private createOpener(id: string, metadata: RegisteredOpenerMetadata): IExternalUriOpener {
return {
id: id,
label: metadata.label,
canOpen: (uri, token) => {
return this.proxy.$canOpenUri(id, uri, token);
},
openExternalUri: async (uri, ctx, token) => {
try {
await this.proxy.$openUri(id, { resolvedUri: uri, sourceUri: ctx.sourceUri }, token);
} catch (e) {
if (!isPromiseCanceledError(e)) {
const openDefaultAction = new Action('default', localize('openerFailedUseDefault', "Open using default opener"), undefined, undefined, () => {
return this.openerService.open(uri, {
allowTunneling: false,
allowContributedOpeners: defaultExternalUriOpenerId,
});
});
openDefaultAction.tooltip = uri.toString();
this.notificationService.notify({
severity: Severity.Error,
message: localize('openerFailedMessage', 'Could not open uri with \'{0}\': {1}', id, e.toString()),
actions: {
primary: [
openDefaultAction
]
}
});
}
}
return true;
},
};
}
async $registerUriOpener(
id: string,
schemes: readonly string[],
extensionId: ExtensionIdentifier,
label: string,
): Promise<void> {
if (this._registeredOpeners.has(id)) {
throw new Error(`Opener with id '${id}' already registered`);
}
this._registeredOpeners.set(id, {
schemes: new Set(schemes),
label,
extensionId,
});
this._contributedExternalUriOpenersStore.add(id, extensionId.value);
}
async $unregisterUriOpener(id: string): Promise<void> {
this._registeredOpeners.delete(id);
this._contributedExternalUriOpenersStore.delete(id);
}
dispose(): void {
super.dispose();
this._registeredOpeners.clear();
}
}

View File

@@ -129,6 +129,9 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
dispose(this._editorProviders.values());
this._editorProviders.clear();
dispose(this._revivers.values());
this._revivers.clear();
}
public get webviewInputs(): Iterable<WebviewInput> { return this._webviewInputs; }
@@ -181,7 +184,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
webview.setName(value);
}
public $setIconPath(handle: extHostProtocol.WebviewHandle, value: { light: UriComponents, dark: UriComponents; } | undefined): void {
const webview = this.getWebviewInput(handle);
webview.iconPath = reviveWebviewIcon(value);
@@ -199,8 +201,7 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
}
}
public $registerSerializer(viewType: string)
: void {
public $registerSerializer(viewType: string): void {
if (this._revivers.has(viewType)) {
throw new Error(`Reviver for ${viewType} already registered`);
}
@@ -216,7 +217,6 @@ export class MainThreadWebviewPanels extends Disposable implements extHostProtoc
return;
}
const handle = webviewInput.id;
this.addWebviewInput(handle, webviewInput);

View File

@@ -78,7 +78,7 @@ export class MainThreadWebviews extends Disposable implements extHostProtocol.Ma
private onDidClickLink(handle: extHostProtocol.WebviewHandle, link: string): void {
const webview = this.getWebview(handle);
if (this.isSupportedLink(webview, URI.parse(link))) {
this._openerService.open(link, { fromUserGesture: true });
this._openerService.open(link, { fromUserGesture: true, allowContributedOpeners: true });
}
}

View File

@@ -52,7 +52,11 @@ export class MainThreadWindow implements MainThreadWindowShape {
// called with URI or transformed -> use uri
target = uri;
}
return this.openerService.open(target, { openExternal: true, allowTunneling: options.allowTunneling });
return this.openerService.open(target, {
openExternal: true,
allowTunneling: options.allowTunneling,
allowContributedOpeners: options.allowContributedOpeners,
});
}
async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise<UriComponents> {

View File

@@ -6,25 +6,25 @@
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { isNative } from 'vs/base/common/platform';
import { withNullAsUndefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { isNative } from 'vs/base/common/platform';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILabelService } from 'vs/platform/label/common/label';
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
import { IWorkspaceContextService, WorkbenchState, IWorkspace, toWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IRequestService } from 'vs/platform/request/common/request';
import { IWorkspace, IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/contrib/search/common/queryBuilder';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IFileMatch, IPatternInfo, ISearchProgressItem, ISearchService } from 'vs/workbench/services/search/common/search';
import { IWorkspaceEditingService } from 'vs/workbench/services/workspaces/common/workspaceEditing';
import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape, IWorkspaceData, ITextSearchComplete } from '../common/extHost.protocol';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isUntitledWorkspace } from 'vs/platform/workspaces/common/workspaces';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IFileService } from 'vs/platform/files/common/files';
import { IRequestService } from 'vs/platform/request/common/request';
import { checkGlobFileExists } from 'vs/workbench/api/common/shared/workspaceContains';
import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, ITextSearchComplete, IWorkspaceData, MainContext, MainThreadWorkspaceShape } from '../common/extHost.protocol';
@extHostNamedCustomer(MainContext.MainThreadWorkspace)
export class MainThreadWorkspace implements MainThreadWorkspaceShape {
@@ -139,7 +139,7 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape {
}
const query = this._queryBuilder.file(
includeFolder ? [toWorkspaceFolder(includeFolder)] : workspace.folders,
includeFolder ? [includeFolder] : workspace.folders,
{
maxResults: withNullAsUndefined(maxResults),
disregardExcludeSettings: (excludePatternOrDisregardExcludes === false) || undefined,

View File

@@ -3,35 +3,32 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { localize } from 'vs/nls';
import { coalesce } from 'vs/base/common/arrays';
import { forEach } from 'vs/base/common/collections';
import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as resources from 'vs/base/common/resources';
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { ViewContainer, IViewsRegistry, ITreeViewDescriptor, IViewContainersRegistry, Extensions as ViewContainerExtensions, TEST_VIEW_CONTAINER_ID, IViewDescriptor, ViewContainerLocation, testViewIcon } from 'vs/workbench/common/views';
import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { coalesce, } from 'vs/base/common/arrays';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from 'vs/workbench/common/contributions';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { Registry } from 'vs/platform/registry/common/platform';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files';
import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm';
import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug';
import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { URI } from 'vs/base/common/uri';
import { ViewletRegistry, Extensions as ViewletExtensions, ShowViewletAction } from 'vs/workbench/browser/viewlet';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { IWorkbenchActionRegistry, Extensions as ActionExtensions, CATEGORIES } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { localize } from 'vs/nls';
import { registerAction2 } from 'vs/platform/actions/common/actions';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Registry } from 'vs/platform/registry/common/platform';
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { CustomTreeView, TreeViewPane } from 'vs/workbench/browser/parts/views/treeView';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { Extensions as ViewletExtensions, ShowViewletAction2, ViewletRegistry } from 'vs/workbench/browser/viewlet';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
import { Extensions as ViewContainerExtensions, ITreeViewDescriptor, IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation } from 'vs/workbench/common/views';
import { VIEWLET_ID as DEBUG } from 'vs/workbench/contrib/debug/common/debug';
import { VIEWLET_ID as EXPLORER } from 'vs/workbench/contrib/files/common/files';
import { VIEWLET_ID as REMOTE } from 'vs/workbench/contrib/remote/browser/remoteExplorer';
import { VIEWLET_ID as SCM } from 'vs/workbench/contrib/scm/common/scm';
import { WebviewViewPane } from 'vs/workbench/contrib/webviewView/browser/webviewViewPane';
import { ExtensionMessageCollector, ExtensionsRegistry, IExtensionPoint, IExtensionPointUser } from 'vs/workbench/services/extensions/common/extensionsRegistry';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
export interface IUserFriendlyViewsContainerDescriptor {
id: string;
@@ -110,7 +107,7 @@ const viewDescriptor: IJSONSchema = {
defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }],
properties: {
type: {
markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."),
markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."),
type: 'string',
enum: [
'tree',
@@ -255,7 +252,8 @@ const viewsExtensionPoint: IExtensionPoint<ViewExtensionPointType> = ExtensionsR
jsonSchema: viewsContribution
});
const TEST_VIEW_CONTAINER_ORDER = 6;
const CUSTOM_VIEWS_START_ORDER = 7;
class ViewsExtensionHandler implements IWorkbenchContribution {
private viewContainersRegistry: IViewContainersRegistry;
@@ -271,7 +269,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}
private handleAndRegisterCustomViewContainers() {
this.registerTestViewContainer();
viewsContainersExtensionPoint.setHandler((extensions, { added, removed }) => {
if (removed.length) {
this.removeCustomViewContainers(removed);
@@ -284,7 +281,7 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
private addCustomViewContainers(extensionPoints: readonly IExtensionPointUser<ViewContainerExtensionPointType>[], existingViewContainers: ViewContainer[]): void {
const viewContainersRegistry = Registry.as<IViewContainersRegistry>(ViewContainerExtensions.ViewContainersRegistry);
let activityBarOrder = TEST_VIEW_CONTAINER_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length + 1;
let activityBarOrder = CUSTOM_VIEWS_START_ORDER + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Sidebar).length;
let panelOrder = 5 + viewContainersRegistry.all.filter(v => !!v.extensionId && viewContainersRegistry.getViewContainerLocation(v) === ViewContainerLocation.Panel).length + 1;
for (let { value, collector, description } of extensionPoints) {
forEach(value, entry => {
@@ -318,13 +315,6 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}
}
private registerTestViewContainer(): void {
const title = localize('test', "Test");
const icon = testViewIcon;
this.registerCustomViewContainer(TEST_VIEW_CONTAINER_ID, title, icon, TEST_VIEW_CONTAINER_ORDER, undefined, ViewContainerLocation.Sidebar);
}
private isValidViewsContainer(viewsContainersDescriptors: IUserFriendlyViewsContainerDescriptor[], collector: ExtensionMessageCollector): boolean {
if (!Array.isArray(viewsContainersDescriptors)) {
collector.error(localize('viewcontainer requirearray', "views containers must be an array"));
@@ -395,22 +385,15 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
}, location);
// Register Action to Open Viewlet
class OpenCustomViewletAction extends ShowViewletAction {
constructor(
id: string, label: string,
@IViewletService viewletService: IViewletService,
@IEditorGroupsService editorGroupService: IEditorGroupsService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService
) {
super(id, label, id, viewletService, editorGroupService, layoutService);
registerAction2(class OpenCustomViewletAction extends ShowViewletAction2 {
constructor() {
super({ id, f1: true, title: localize('showViewlet', "Show {0}", title), category: CATEGORIES.View.value });
}
}
const registry = Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions);
registry.registerWorkbenchAction(
SyncActionDescriptor.create(OpenCustomViewletAction, id, localize('showViewlet', "Show {0}", title)),
`View: Show ${title}`,
CATEGORIES.View.value
);
protected viewletId() {
return id;
}
});
}
return viewContainer;
@@ -501,7 +484,8 @@ class ViewsExtensionHandler implements IWorkbenchContribution {
originalContainerId: entry.key,
group: item.group,
remoteAuthority: item.remoteName || (<any>item).remoteAuthority, // TODO@roblou - delete after remote extensions are updated
hideByDefault: initialVisibility === InitialVisibility.Hidden
hideByDefault: initialVisibility === InitialVisibility.Hidden,
workspace: viewContainer?.id === REMOTE ? true : undefined
};

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI, UriComponents } from 'vs/base/common/uri';
import { URI } from 'vs/base/common/uri';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { CommandsRegistry, ICommandService, ICommandHandler } from 'vs/platform/commands/common/commands';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
@@ -12,7 +12,6 @@ import { IWorkspacesService, IRecent } from 'vs/platform/workspaces/common/works
import { ILogService } from 'vs/platform/log/common/log';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IViewDescriptorService, IViewsService, ViewVisibilityState } from 'vs/workbench/common/views';
import { IOpenerService } from 'vs/platform/opener/common/opener';
// -----------------------------------------------------------------
// The following commands are registered on both sides separately.
@@ -128,12 +127,6 @@ CommandsRegistry.registerCommand('_extensionTests.setLogLevel', function (access
}
});
CommandsRegistry.registerCommand('_workbench.openExternal', function (accessor: ServicesAccessor, uri: UriComponents, options: { allowTunneling?: boolean }) {
// TODO: discuss martin, ben where to put this
const openerService = accessor.get(IOpenerService);
openerService.open(URI.revive(uri), options);
});
CommandsRegistry.registerCommand('_extensionTests.getLogLevel', function (accessor: ServicesAccessor) {
const logService = accessor.get(ILogService);

View File

@@ -0,0 +1,38 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtHostSecretStateShape, MainContext, MainThreadSecretStateShape } from 'vs/workbench/api/common/extHost.protocol';
import { Emitter } from 'vs/base/common/event';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export class ExtHostSecretState implements ExtHostSecretStateShape {
private _proxy: MainThreadSecretStateShape;
private _onDidChangePassword = new Emitter<{ extensionId: string, key: string }>();
readonly onDidChangePassword = this._onDidChangePassword.event;
constructor(mainContext: IExtHostRpcService) {
this._proxy = mainContext.getProxy(MainContext.MainThreadSecretState);
}
async $onDidChangePassword(e: { extensionId: string, key: string }): Promise<void> {
this._onDidChangePassword.fire(e);
}
get(extensionId: string, key: string): Promise<string | undefined> {
return this._proxy.$getPassword(extensionId, key);
}
store(extensionId: string, key: string, value: string): Promise<void> {
return this._proxy.$setPassword(extensionId, key, value);
}
delete(extensionId: string, key: string): Promise<void> {
return this._proxy.$deletePassword(extensionId, key);
}
}
export interface IExtHostSecretState extends ExtHostSecretState { }
export const IExtHostSecretState = createDecorator<IExtHostSecretState>('IExtHostSecretState');

View File

@@ -82,6 +82,9 @@ import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPane
import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits';
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting';
import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener';
import { IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState';
import { ExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';
export interface IExtensionApiFactory {
(extension: IExtensionDescription, registry: ExtensionDescriptionRegistry, configProvider: ExtHostConfigProvider): typeof vscode;
@@ -107,6 +110,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostTunnelService = accessor.get(IExtHostTunnelService);
const extHostApiDeprecation = accessor.get(IExtHostApiDeprecationService);
const extHostWindow = accessor.get(IExtHostWindow);
const extHostSecretState = accessor.get(IExtHostSecretState);
// register addressable instances
rpcProtocol.set(ExtHostContext.ExtHostFileSystemInfo, extHostFileSystemInfo);
@@ -117,6 +121,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
rpcProtocol.set(ExtHostContext.ExtHostStorage, extHostStorage);
rpcProtocol.set(ExtHostContext.ExtHostTunnelService, extHostTunnelService);
rpcProtocol.set(ExtHostContext.ExtHostWindow, extHostWindow);
rpcProtocol.set(ExtHostContext.ExtHostSecretState, extHostSecretState);
// automatically create and register addressable instances
const extHostDecorations = rpcProtocol.set(ExtHostContext.ExtHostDecorations, accessor.get(IExtHostDecorations));
@@ -129,6 +134,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostOutputService = rpcProtocol.set(ExtHostContext.ExtHostOutputService, accessor.get(IExtHostOutputService));
// manually create and register addressable instances
const extHostEditorTabs = rpcProtocol.set(ExtHostContext.ExtHostEditorTabs, new ExtHostEditorTabs());
const extHostUrls = rpcProtocol.set(ExtHostContext.ExtHostUrls, new ExtHostUrls(rpcProtocol));
const extHostDocuments = rpcProtocol.set(ExtHostContext.ExtHostDocuments, new ExtHostDocuments(rpcProtocol, extHostDocumentsAndEditors));
const extHostDocumentContentProviders = rpcProtocol.set(ExtHostContext.ExtHostDocumentContentProviders, new ExtHostDocumentContentProvider(rpcProtocol, extHostDocumentsAndEditors, extHostLogService));
@@ -154,6 +160,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostCustomEditors = rpcProtocol.set(ExtHostContext.ExtHostCustomEditors, new ExtHostCustomEditors(rpcProtocol, extHostDocuments, extensionStoragePaths, extHostWebviews, extHostWebviewPanels));
const extHostWebviewViews = rpcProtocol.set(ExtHostContext.ExtHostWebviewViews, new ExtHostWebviewViews(rpcProtocol, extHostWebviews));
const extHostTesting = rpcProtocol.set(ExtHostContext.ExtHostTesting, new ExtHostTesting(rpcProtocol, extHostDocumentsAndEditors, extHostWorkspace));
const extHostUriOpeners = rpcProtocol.set(ExtHostContext.ExtHostUriOpeners, new ExtHostUriOpeners(rpcProtocol));
// Check that no named customers are missing
const expected: ProxyIdentifier<any>[] = values(ExtHostContext);
@@ -209,22 +216,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
get onDidChangeSessions(): Event<vscode.AuthenticationSessionsChangeEvent> {
return extHostAuthentication.onDidChangeSessions;
},
registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable {
registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable {
checkProposedApiEnabled(extension);
return extHostAuthentication.registerAuthenticationProvider(provider);
return extHostAuthentication.registerAuthenticationProvider(id, label, provider, options);
},
get onDidChangeAuthenticationProviders(): Event<vscode.AuthenticationProvidersChangeEvent> {
checkProposedApiEnabled(extension);
return extHostAuthentication.onDidChangeAuthenticationProviders;
},
getProviderIds(): Thenable<ReadonlyArray<string>> {
checkProposedApiEnabled(extension);
return extHostAuthentication.getProviderIds();
},
get providerIds(): string[] {
checkProposedApiEnabled(extension);
return extHostAuthentication.providerIds;
},
get providers(): ReadonlyArray<vscode.AuthenticationProviderInformation> {
checkProposedApiEnabled(extension);
return extHostAuthentication.providers;
@@ -232,22 +231,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
logout(providerId: string, sessionId: string): Thenable<void> {
checkProposedApiEnabled(extension);
return extHostAuthentication.logout(providerId, sessionId);
},
getPassword(key: string): Thenable<string | undefined> {
checkProposedApiEnabled(extension);
return extHostAuthentication.getPassword(extension, key);
},
setPassword(key: string, value: string): Thenable<void> {
checkProposedApiEnabled(extension);
return extHostAuthentication.setPassword(extension, key, value);
},
deletePassword(key: string): Thenable<void> {
checkProposedApiEnabled(extension);
return extHostAuthentication.deletePassword(extension, key);
},
get onDidChangePassword(): Event<void> {
checkProposedApiEnabled(extension);
return extHostAuthentication.onDidChangePassword;
}
};
@@ -311,8 +294,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
get shell() {
return extHostTerminalService.getDefaultShell(false, configProvider);
},
openExternal(uri: URI) {
return extHostWindow.openUri(uri, { allowTunneling: !!initData.remote.authority });
openExternal(uri: URI, options?: { allowContributedOpeners?: boolean | string; }) {
return extHostWindow.openUri(uri, {
allowTunneling: !!initData.remote.authority,
allowContributedOpeners: options?.allowContributedOpeners,
});
},
asExternalUri(uri: URI) {
if (uri.scheme === initData.environment.appUriScheme) {
@@ -354,6 +340,14 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension);
return extHostTesting.runTests(provider);
},
get onDidChangeTestResults() {
checkProposedApiEnabled(extension);
return extHostTesting.onLastResultsChanged;
},
get testResults() {
checkProposedApiEnabled(extension);
return extHostTesting.lastResults;
},
};
// namespace: extensions
@@ -480,6 +474,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) {
checkProposedApiEnabled(extension);
return extHostLanguages.tokenAtPosition(doc, pos);
},
registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable {
checkProposedApiEnabled(extension);
return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider);
}
};
@@ -588,10 +586,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
id = extension.identifier.value;
name = nls.localize('extensionLabel', "{0} (Extension)", extension.displayName || extension.name);
alignment = alignmentOrOptions;
priority = priority;
}
return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation, extension);
return extHostStatusBar.createStatusBarEntry(id, name, alignment, priority, accessibilityInformation);
},
setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable<any>): vscode.Disposable {
return extHostStatusBar.setStatusBarMessage(text, timeoutOrThenable);
@@ -691,6 +688,18 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
showNotebookDocument(document, options?) {
checkProposedApiEnabled(extension);
return extHostNotebook.showNotebookDocument(document, options);
},
registerExternalUriOpener(id: string, opener: vscode.ExternalUriOpener, metadata: vscode.ExternalUriOpenerMetadata) {
checkProposedApiEnabled(extension);
return extHostUriOpeners.registerExternalUriOpener(extension.identifier, id, opener, metadata);
},
get openEditors() {
checkProposedApiEnabled(extension);
return extHostEditorTabs.tabs;
},
get onDidChangeOpenEditors() {
checkProposedApiEnabled(extension);
return extHostEditorTabs.onDidChangeTabs;
}
};
@@ -1108,6 +1117,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
CallHierarchyIncomingCall: extHostTypes.CallHierarchyIncomingCall,
CallHierarchyItem: extHostTypes.CallHierarchyItem,
CallHierarchyOutgoingCall: extHostTypes.CallHierarchyOutgoingCall,
CancellationError: errors.CancellationError,
CancellationTokenSource: CancellationTokenSource,
CodeAction: extHostTypes.CodeAction,
CodeActionKind: extHostTypes.CodeActionKind,
@@ -1148,6 +1158,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
EventEmitter: Emitter,
ExtensionKind: extHostTypes.ExtensionKind,
ExtensionMode: extHostTypes.ExtensionMode,
ExternalUriOpenerPriority: extHostTypes.ExternalUriOpenerPriority,
FileChangeType: extHostTypes.FileChangeType,
FileDecoration: extHostTypes.FileDecoration,
FileSystemError: extHostTypes.FileSystemError,
@@ -1157,6 +1168,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
FunctionBreakpoint: extHostTypes.FunctionBreakpoint,
Hover: extHostTypes.Hover,
IndentAction: languageConfiguration.IndentAction,
InlineHint: extHostTypes.InlineHint,
Location: extHostTypes.Location,
MarkdownString: extHostTypes.MarkdownString,
OverviewRulerLane: OverviewRulerLane,

View File

@@ -21,6 +21,7 @@ import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs
import { IExtHostWindow, ExtHostWindow } from 'vs/workbench/api/common/extHostWindow';
import { IExtHostConsumerFileSystem, ExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
import { IExtHostFileSystemInfo, ExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState';
registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService);
@@ -39,3 +40,4 @@ registerSingleton(IExtHostTerminalService, WorkerExtHostTerminalService);
registerSingleton(IExtHostTunnelService, ExtHostTunnelService);
registerSingleton(IExtHostWindow, ExtHostWindow);
registerSingleton(IExtHostWorkspace, ExtHostWorkspace);
registerSingleton(IExtHostSecretState, ExtHostSecretState);

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as performance from 'vs/base/common/performance';
import { VSBuffer } from 'vs/base/common/buffer';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IRemoteConsoleLog } from 'vs/base/common/console';
@@ -41,13 +42,13 @@ import { IRevealOptions, ITreeItem } from 'vs/workbench/common/views';
import { IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug';
import { ITextQueryBuilderOptions } from 'vs/workbench/contrib/search/common/queryBuilder';
import { ITerminalDimensions, IShellLaunchConfig, ITerminalLaunchError } from 'vs/workbench/contrib/terminal/common/terminal';
import { ActivationKind, ExtensionActivationError } from 'vs/workbench/services/extensions/common/extensions';
import { ActivationKind, ExtensionActivationError, ExtensionHostKind } from 'vs/workbench/services/extensions/common/extensions';
import { createExtHostContextProxyIdentifier as createExtId, createMainContextProxyIdentifier as createMainId, IRPCProtocol } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import * as search from 'vs/workbench/services/search/common/search';
import { EditorGroupColumn, SaveReason } from 'vs/workbench/common/editor';
import { ExtensionActivationReason } from 'vs/workbench/api/common/extHostExtensionActivator';
import { TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { TunnelCreationOptions, TunnelProviderFeatures, TunnelOptions } from 'vs/platform/remote/common/tunnel';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor, InternalTimelineOptions } from 'vs/workbench/contrib/timeline/common/timeline';
import { revive } from 'vs/base/common/marshalling';
import { IProcessedOutput, INotebookDisplayOrder, NotebookCellMetadata, NotebookDocumentMetadata, ICellEditOperation, NotebookCellsChangedEventDto, NotebookDataDto, IMainCellDto, INotebookDocumentFilter, INotebookKernelInfoDto2, TransientMetadata, INotebookCellStatusBarEntry, ICellRange, INotebookDecorationRenderOptions, INotebookExclusiveDocumentFilter } from 'vs/workbench/contrib/notebook/common/notebookCommon';
@@ -57,7 +58,8 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/api/common/extHostTypes';
import { IAccessibilityInformation } from 'vs/platform/accessibility/common/accessibility';
import { IExtensionIdWithVersion } from 'vs/platform/userDataSync/common/extensionsStorageSync';
import { RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { InternalTestItem, InternalTestResults, RunTestForProviderRequest, RunTestsRequest, RunTestsResult, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
export interface IEnvironment {
isExtensionDevelopmentDebug: boolean;
@@ -108,7 +110,8 @@ export interface IConfigurationInitData extends IConfigurationData {
}
export interface IExtHostContext extends IRPCProtocol {
remoteAuthority: string | null;
readonly remoteAuthority: string | null;
readonly extensionHostKind: ExtensionHostKind;
}
export interface IMainContext extends IRPCProtocol {
@@ -174,7 +177,9 @@ export interface MainThreadAuthenticationShape extends IDisposable {
$getSessions(providerId: string): Promise<ReadonlyArray<modes.AuthenticationSession>>;
$login(providerId: string, scopes: string[]): Promise<modes.AuthenticationSession>;
$logout(providerId: string, sessionId: string): Promise<void>;
}
export interface MainThreadSecretStateShape extends IDisposable {
$getPassword(extensionId: string, key: string): Promise<string | undefined>;
$setPassword(extensionId: string, key: string, value: string): Promise<void>;
$deletePassword(extensionId: string, key: string): Promise<void>;
@@ -330,7 +335,7 @@ export interface IIndentationRuleDto {
export interface IOnEnterRuleDto {
beforeText: IRegExpDto;
afterText?: IRegExpDto;
oneLineAboveText?: IRegExpDto;
previousLineText?: IRegExpDto;
action: EnterAction;
}
export interface ILanguageConfigurationDto {
@@ -397,6 +402,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable {
$registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void;
$registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void;
$registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void;
$registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void;
$emitInlineHintsEvent(eventHandle: number, event?: any): void;
$registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void;
$registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void;
$registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void;
@@ -415,6 +422,7 @@ export interface MainThreadLanguagesShape extends IDisposable {
export interface MainThreadMessageOptions {
extension?: IExtensionDescription;
modal?: boolean;
useCustom?: boolean;
}
export interface MainThreadMessageServiceShape extends IDisposable {
@@ -438,6 +446,16 @@ export interface MainThreadProgressShape extends IDisposable {
$progressEnd(handle: number): void;
}
/**
* A terminal that is created on the extension host side is temporarily assigned
* a UUID by the extension host that created it. Once the renderer side has assigned
* a real numeric id, the numeric id will be used.
*
* All other terminals (that are not created on the extension host side) always
* use the numeric id.
*/
export type TerminalIdentifier = number | string;
export interface TerminalLaunchConfig {
name?: string;
shellPath?: string;
@@ -452,11 +470,11 @@ export interface TerminalLaunchConfig {
}
export interface MainThreadTerminalServiceShape extends IDisposable {
$createTerminal(config: TerminalLaunchConfig): Promise<{ id: number, name: string; }>;
$dispose(terminalId: number): void;
$hide(terminalId: number): void;
$sendText(terminalId: number, text: string, addNewLine: boolean): void;
$show(terminalId: number, preserveFocus: boolean): void;
$createTerminal(extHostTerminalId: string, config: TerminalLaunchConfig): Promise<void>;
$dispose(id: TerminalIdentifier): void;
$hide(id: TerminalIdentifier): void;
$sendText(id: TerminalIdentifier, text: string, addNewLine: boolean): void;
$show(id: TerminalIdentifier, preserveFocus: boolean): void;
$startSendingDataEvents(): void;
$stopSendingDataEvents(): void;
$startLinkProvider(): void;
@@ -594,6 +612,24 @@ export interface ExtHostEditorInsetsShape {
$onDidReceiveMessage(handle: number, message: any): void;
}
//#region --- open editors model
export interface MainThreadEditorTabsShape extends IDisposable {
// manage tabs: move, close, rearrange etc
}
export interface IEditorTabDto {
group: number;
name: string;
resource: UriComponents
}
export interface IExtHostEditorTabsShape {
$acceptEditorTabs(tabs: IEditorTabDto[]): void;
}
//#endregion
export type WebviewHandle = string;
export interface WebviewPanelShowOptions {
@@ -739,6 +775,7 @@ export enum NotebookEditorRevealType {
Default = 0,
InCenter = 1,
InCenterIfOutsideViewport = 2,
AtTop = 3
}
export interface INotebookDocumentShowOptions {
@@ -786,6 +823,16 @@ export interface ExtHostUrlsShape {
$handleExternalUri(handle: number, uri: UriComponents): Promise<void>;
}
export interface MainThreadUriOpenersShape extends IDisposable {
$registerUriOpener(id: string, schemes: readonly string[], extensionId: ExtensionIdentifier, label: string): Promise<void>;
$unregisterUriOpener(id: string): Promise<void>;
}
export interface ExtHostUriOpenersShape {
$canOpenUri(id: string, uri: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerPriority>;
$openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void>;
}
export interface ITextSearchComplete {
limitHit?: boolean;
}
@@ -853,6 +900,7 @@ export interface MainThreadExtensionServiceShape extends IDisposable {
$onExtensionActivationError(extensionId: ExtensionIdentifier, error: ExtensionActivationError): Promise<void>;
$onExtensionRuntimeError(extensionId: ExtensionIdentifier, error: SerializedError): void;
$onExtensionHostExit(code: number): Promise<void>;
$setPerformanceMarks(marks: performance.PerformanceMark[]): Promise<void>;
}
export interface SCMProviderFeatures {
@@ -946,6 +994,7 @@ export interface MainThreadDebugServiceShape extends IDisposable {
export interface IOpenUriOptions {
readonly allowTunneling?: boolean;
readonly allowContributedOpeners?: boolean | string;
}
export interface MainThreadWindowShape extends IDisposable {
@@ -958,8 +1007,9 @@ export interface MainThreadTunnelServiceShape extends IDisposable {
$openTunnel(tunnelOptions: TunnelOptions, source: string | undefined): Promise<TunnelDto | undefined>;
$closeTunnel(remote: { host: string, port: number }): Promise<void>;
$getTunnels(): Promise<TunnelDescription[]>;
$setTunnelProvider(): Promise<void>;
$tunnelServiceReady(): Promise<void>;
$setTunnelProvider(features: TunnelProviderFeatures): Promise<void>;
$setCandidateFinder(): Promise<void>;
$setCandidateFilter(): Promise<void>;
$onFoundNewCandidates(candidates: { host: string, port: number, detail: string }[]): Promise<void>;
}
@@ -1052,7 +1102,7 @@ export interface ExtHostTreeViewsShape {
$setSelection(treeViewId: string, treeItemHandles: string[]): void;
$setVisible(treeViewId: string, visible: boolean): void;
$hasResolve(treeViewId: string): Promise<boolean>;
$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined>;
$resolve(treeViewId: string, treeItemHandle: string, token: CancellationToken): Promise<ITreeItem | undefined>;
}
export interface ExtHostWorkspaceShape {
@@ -1094,7 +1144,10 @@ export interface ExtHostAuthenticationShape {
$onDidChangeAuthenticationSessions(id: string, label: string, event: modes.AuthenticationSessionsChangeEvent): Promise<void>;
$onDidChangeAuthenticationProviders(added: modes.AuthenticationProviderInformation[], removed: modes.AuthenticationProviderInformation[]): Promise<void>;
$setProviders(providers: modes.AuthenticationProviderInformation[]): Promise<void>;
$onDidChangePassword(): Promise<void>;
}
export interface ExtHostSecretStateShape {
$onDidChangePassword(e: { extensionId: string, key: string }): Promise<void>;
}
export interface ExtHostSearchShape {
@@ -1145,9 +1198,14 @@ export interface SourceTargetPair {
target: UriComponents;
}
export interface IWillRunFileOperationParticipation {
edit: IWorkspaceEditDto;
extensionNames: string[]
}
export interface ExtHostFileSystemEventServiceShape {
$onFileEvent(events: FileSystemEvents): void;
$onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise<any>;
$onWillRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined>;
$onDidRunFileOperation(operation: files.FileOperation, files: SourceTargetPair[]): void;
}
@@ -1252,6 +1310,18 @@ export interface ISignatureHelpContextDto {
readonly activeSignatureHelp?: ISignatureHelpDto;
}
export interface IInlineHintDto {
text: string;
range: IRange;
hoverMessage?: string;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
}
export interface IInlineHintsDto {
hints: IInlineHintDto[]
}
export interface ILocationDto {
uri: UriComponents;
range: IRange;
@@ -1441,6 +1511,7 @@ export interface ExtHostLanguageFeaturesShape {
$releaseCompletionItems(handle: number, id: number): void;
$provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise<ISignatureHelpDto | undefined>;
$releaseSignatureHelp(handle: number, id: number): void;
$provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise<IInlineHintsDto | undefined>
$provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise<ILinksListDto | undefined>;
$resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise<ILinkDto | undefined>;
$releaseDocumentLinks(handle: number, id: number): void;
@@ -1473,6 +1544,7 @@ export interface IShellLaunchConfigDto {
cwd?: string | UriComponents;
env?: { [key: string]: string | null; };
hideFromUser?: boolean;
flowControl?: boolean;
}
export interface IShellDefinitionDto {
@@ -1503,7 +1575,7 @@ export interface ITerminalDimensionsDto {
export interface ExtHostTerminalServiceShape {
$acceptTerminalClosed(id: number, exitCode: number | undefined): void;
$acceptTerminalOpened(id: number, name: string, shellLaunchConfig: IShellLaunchConfigDto): void;
$acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfig: IShellLaunchConfigDto): void;
$acceptActiveTerminalChanged(id: number | null): void;
$acceptTerminalProcessId(id: number, processId: number): void;
$acceptTerminalProcessData(id: number, data: string): void;
@@ -1512,6 +1584,7 @@ export interface ExtHostTerminalServiceShape {
$acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): void;
$spawnExtHostProcess(id: number, shellLaunchConfig: IShellLaunchConfigDto, activeWorkspaceRootUri: UriComponents | undefined, cols: number, rows: number, isWorkspaceShellAllowed: boolean): Promise<ITerminalLaunchError | undefined>;
$startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<ITerminalLaunchError | undefined>;
$acceptProcessAckDataEvent(id: number, charCount: number): void;
$acceptProcessInput(id: number, data: string): void;
$acceptProcessResize(id: number, cols: number, rows: number): void;
$acceptProcessShutdown(id: number, immediate: boolean): void;
@@ -1725,7 +1798,7 @@ export interface ExtHostNotebookShape {
$saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean>;
$backup(viewType: string, uri: UriComponents, cancellation: CancellationToken): Promise<string | undefined>;
$acceptDisplayOrder(displayOrder: INotebookDisplayOrder): void;
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }): void;
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined }): void;
$onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void;
$acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEventDto, isDirty: boolean): void;
$acceptModelSaved(uriComponents: UriComponents): void;
@@ -1749,9 +1822,11 @@ export interface MainThreadThemingShape extends IDisposable {
}
export interface ExtHostTunnelServiceShape {
$forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto> | undefined;
$forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto | undefined>;
$closeTunnel(remote: { host: string, port: number }, silent?: boolean): Promise<void>;
$onDidTunnelsChange(): Promise<void>;
$registerCandidateFinder(enable: boolean): Promise<void>;
$applyCandidateFilter(candidates: CandidatePort[]): Promise<CandidatePort[]>;
}
export interface ExtHostTimelineShape {
@@ -1764,11 +1839,12 @@ export const enum ExtHostTestingResource {
}
export interface ExtHostTestingShape {
$runTestsForProvider(req: RunTestForProviderRequest): Promise<RunTestsResult>;
$runTestsForProvider(req: RunTestForProviderRequest, token: CancellationToken): Promise<RunTestsResult>;
$subscribeToTests(resource: ExtHostTestingResource, uri: UriComponents): void;
$unsubscribeFromTests(resource: ExtHostTestingResource, uri: UriComponents): void;
$lookupTest(test: TestIdWithProvider): Promise<InternalTestItem | undefined>;
$acceptDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$publishTestResults(results: InternalTestResults): void;
}
export interface MainThreadTestingShape {
@@ -1777,7 +1853,7 @@ export interface MainThreadTestingShape {
$subscribeToDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
$unsubscribeFromDiffs(resource: ExtHostTestingResource, uri: UriComponents): void;
$publishDiff(resource: ExtHostTestingResource, uri: UriComponents, diff: TestsDiff): void;
$runTests(req: RunTestsRequest): Promise<RunTestsResult>;
$runTests(req: RunTestsRequest, token: CancellationToken): Promise<RunTestsResult>;
}
// --- proxy identifiers
@@ -1798,6 +1874,7 @@ export const MainContext = {
MainThreadDocumentContentProviders: createMainId<MainThreadDocumentContentProvidersShape>('MainThreadDocumentContentProviders'),
MainThreadTextEditors: createMainId<MainThreadTextEditorsShape>('MainThreadTextEditors'),
MainThreadEditorInsets: createMainId<MainThreadEditorInsetsShape>('MainThreadEditorInsets'),
MainThreadEditorTabs: createMainId<MainThreadEditorTabsShape>('MainThreadEditorTabs'),
MainThreadErrors: createMainId<MainThreadErrorsShape>('MainThreadErrors'),
MainThreadTreeViews: createMainId<MainThreadTreeViewsShape>('MainThreadTreeViews'),
MainThreadDownloadService: createMainId<MainThreadDownloadServiceShape>('MainThreadDownloadService'),
@@ -1810,6 +1887,7 @@ export const MainContext = {
MainThreadProgress: createMainId<MainThreadProgressShape>('MainThreadProgress'),
MainThreadQuickOpen: createMainId<MainThreadQuickOpenShape>('MainThreadQuickOpen'),
MainThreadStatusBar: createMainId<MainThreadStatusBarShape>('MainThreadStatusBar'),
MainThreadSecretState: createMainId<MainThreadSecretStateShape>('MainThreadSecretState'),
MainThreadStorage: createMainId<MainThreadStorageShape>('MainThreadStorage'),
MainThreadTelemetry: createMainId<MainThreadTelemetryShape>('MainThreadTelemetry'),
MainThreadTerminalService: createMainId<MainThreadTerminalServiceShape>('MainThreadTerminalService'),
@@ -1818,6 +1896,7 @@ export const MainContext = {
MainThreadWebviewViews: createMainId<MainThreadWebviewViewsShape>('MainThreadWebviewViews'),
MainThreadCustomEditors: createMainId<MainThreadCustomEditorsShape>('MainThreadCustomEditors'),
MainThreadUrls: createMainId<MainThreadUrlsShape>('MainThreadUrls'),
MainThreadUriOpeners: createMainId<MainThreadUriOpenersShape>('MainThreadUriOpeners'),
MainThreadWorkspace: createMainId<MainThreadWorkspaceShape>('MainThreadWorkspace'),
MainThreadFileSystem: createMainId<MainThreadFileSystemShape>('MainThreadFileSystem'),
MainThreadExtensionService: createMainId<MainThreadExtensionServiceShape>('MainThreadExtensionService'),
@@ -1863,10 +1942,13 @@ export const ExtHostContext = {
ExtHostCustomEditors: createExtId<ExtHostCustomEditorsShape>('ExtHostCustomEditors'),
ExtHostWebviewViews: createExtId<ExtHostWebviewViewsShape>('ExtHostWebviewViews'),
ExtHostEditorInsets: createExtId<ExtHostEditorInsetsShape>('ExtHostEditorInsets'),
ExtHostEditorTabs: createExtId<IExtHostEditorTabsShape>('ExtHostEditorTabs'),
ExtHostProgress: createMainId<ExtHostProgressShape>('ExtHostProgress'),
ExtHostComments: createMainId<ExtHostCommentsShape>('ExtHostComments'),
ExtHostSecretState: createMainId<ExtHostSecretStateShape>('ExtHostSecretState'),
ExtHostStorage: createMainId<ExtHostStorageShape>('ExtHostStorage'),
ExtHostUrls: createExtId<ExtHostUrlsShape>('ExtHostUrls'),
ExtHostUriOpeners: createExtId<ExtHostUriOpenersShape>('ExtHostUriOpeners'),
ExtHostOutputService: createMainId<ExtHostOutputServiceShape>('ExtHostOutputService'),
ExtHosLabelService: createMainId<ExtHostLabelServiceShape>('ExtHostLabelService'),
ExtHostNotebook: createMainId<ExtHostNotebookShape>('ExtHostNotebook'),

View File

@@ -20,6 +20,8 @@ import { IRange } from 'vs/editor/common/core/range';
import { IPosition } from 'vs/editor/common/core/position';
import { TransientMetadata } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { VSBuffer } from 'vs/base/common/buffer';
import { decodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto';
//#region --- NEW world
@@ -176,6 +178,57 @@ const newCommands: ApiCommand[] = [
[ApiCommandArgument.Uri, ApiCommandArgument.Number.with('linkResolveCount', 'Number of links that should be resolved, only when links are unresolved.').optional()],
new ApiCommandResult<modes.ILink[], vscode.DocumentLink[]>('A promise that resolves to an array of DocumentLink-instances.', value => value.map(typeConverters.DocumentLink.to))
),
// --- semantic tokens
new ApiCommand(
'vscode.provideDocumentSemanticTokensLegend', '_provideDocumentSemanticTokensLegend', 'Provide semantic tokens legend for a document',
[ApiCommandArgument.Uri],
new ApiCommandResult<modes.SemanticTokensLegend, types.SemanticTokensLegend | undefined>('A promise that resolves to SemanticTokensLegend.', value => {
if (!value) {
return undefined;
}
return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers);
})
),
new ApiCommand(
'vscode.provideDocumentSemanticTokens', '_provideDocumentSemanticTokens', 'Provide semantic tokens for a document',
[ApiCommandArgument.Uri],
new ApiCommandResult<VSBuffer, types.SemanticTokens | undefined>('A promise that resolves to SemanticTokens.', value => {
if (!value) {
return undefined;
}
const semanticTokensDto = decodeSemanticTokensDto(value);
if (semanticTokensDto.type !== 'full') {
// only accepting full semantic tokens from provideDocumentSemanticTokens
return undefined;
}
return new types.SemanticTokens(semanticTokensDto.data, undefined);
})
),
new ApiCommand(
'vscode.provideDocumentRangeSemanticTokensLegend', '_provideDocumentRangeSemanticTokensLegend', 'Provide semantic tokens legend for a document range',
[ApiCommandArgument.Uri],
new ApiCommandResult<modes.SemanticTokensLegend, types.SemanticTokensLegend | undefined>('A promise that resolves to SemanticTokensLegend.', value => {
if (!value) {
return undefined;
}
return new types.SemanticTokensLegend(value.tokenTypes, value.tokenModifiers);
})
),
new ApiCommand(
'vscode.provideDocumentRangeSemanticTokens', '_provideDocumentRangeSemanticTokens', 'Provide semantic tokens for a document range',
[ApiCommandArgument.Uri, ApiCommandArgument.Range],
new ApiCommandResult<VSBuffer, types.SemanticTokens | undefined>('A promise that resolves to SemanticTokens.', value => {
if (!value) {
return undefined;
}
const semanticTokensDto = decodeSemanticTokensDto(value);
if (semanticTokensDto.type !== 'full') {
// only accepting full semantic tokens from provideDocumentRangeSemanticTokens
return undefined;
}
return new types.SemanticTokens(semanticTokensDto.data, undefined);
})
),
// --- completions
new ApiCommand(
'vscode.executeCompletionItemProvider', '_executeCompletionItemProvider', 'Execute completion item provider.',
@@ -271,13 +324,21 @@ const newCommands: ApiCommand[] = [
return [];
})
),
// --- inline hints
new ApiCommand(
'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider',
[ApiCommandArgument.Uri, ApiCommandArgument.Range],
new ApiCommandResult<modes.InlineHint[], vscode.InlineHint[]>('A promise that resolves to an array of InlineHint objects', result => {
return result.map(typeConverters.InlineHint.to);
})
),
// --- notebooks
new ApiCommand(
'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers',
[
new ApiCommandArgument<string, string>('viewType', '', v => typeof v === 'string', v => v),
new ApiCommandArgument<string, string>('displayName', '', v => typeof v === 'string', v => v),
new ApiCommandArgument<object, object>('options', '', v => typeof v === 'object', v => v),
// new ApiCommandArgument<string, string>('viewType', '', v => typeof v === 'string', v => v),
// new ApiCommandArgument<string, string>('displayName', '', v => typeof v === 'string', v => v),
// new ApiCommandArgument<object, object>('options', '', v => typeof v === 'object', v => v),
],
new ApiCommandResult<{
viewType: string;

View File

@@ -15,11 +15,15 @@ interface GetSessionsRequest {
result: Promise<vscode.AuthenticationSession | undefined>;
}
interface ProviderWithMetadata {
label: string;
provider: vscode.AuthenticationProvider;
options: vscode.AuthenticationProviderOptions;
}
export class ExtHostAuthentication implements ExtHostAuthenticationShape {
private _proxy: MainThreadAuthenticationShape;
private _authenticationProviders: Map<string, vscode.AuthenticationProvider> = new Map<string, vscode.AuthenticationProvider>();
private _providerIds: string[] = [];
private _authenticationProviders: Map<string, ProviderWithMetadata> = new Map<string, ProviderWithMetadata>();
private _providers: vscode.AuthenticationProviderInformation[] = [];
@@ -29,9 +33,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
private _onDidChangeSessions = new Emitter<vscode.AuthenticationSessionsChangeEvent>();
readonly onDidChangeSessions: Event<vscode.AuthenticationSessionsChangeEvent> = this._onDidChangeSessions.event;
private _onDidChangePassword = new Emitter<void>();
readonly onDidChangePassword: Event<void> = this._onDidChangePassword.event;
private _inFlightRequests = new Map<string, GetSessionsRequest[]>();
constructor(mainContext: IMainContext) {
@@ -43,14 +44,6 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
return Promise.resolve();
}
getProviderIds(): Promise<ReadonlyArray<string>> {
return this._proxy.$getProviderIds();
}
get providerIds(): string[] {
return this._providerIds;
}
get providers(): ReadonlyArray<vscode.AuthenticationProviderInformation> {
return Object.freeze(this._providers.slice());
}
@@ -90,128 +83,120 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
private async _getSession(requestingExtension: IExtensionDescription, extensionId: string, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise<vscode.AuthenticationSession | undefined> {
await this._proxy.$ensureProvider(providerId);
const provider = this._authenticationProviders.get(providerId);
const providerData = this._authenticationProviders.get(providerId);
const extensionName = requestingExtension.displayName || requestingExtension.name;
if (!provider) {
if (!providerData) {
return this._proxy.$getSession(providerId, scopes, extensionId, extensionName, options);
}
const orderedScopes = scopes.sort().join(' ');
const sessions = (await provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes);
const sessions = (await providerData.provider.getSessions()).filter(session => session.scopes.slice().sort().join(' ') === orderedScopes);
let session: vscode.AuthenticationSession | undefined = undefined;
if (sessions.length) {
if (!provider.supportsMultipleAccounts) {
if (!providerData.options.supportsMultipleAccounts) {
session = sessions[0];
const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, provider.label, extensionId, extensionName);
const allowed = await this._proxy.$getSessionsPrompt(providerId, session.account.label, providerData.label, extensionId, extensionName);
if (!allowed) {
throw new Error('User did not consent to login.');
}
} else {
// On renderer side, confirm consent, ask user to choose between accounts if multiple sessions are valid
const selected = await this._proxy.$selectSession(providerId, provider.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference);
const selected = await this._proxy.$selectSession(providerId, providerData.label, extensionId, extensionName, sessions, scopes, !!options.clearSessionPreference);
session = sessions.find(session => session.id === selected.id);
}
} else {
if (options.createIfNone) {
const isAllowed = await this._proxy.$loginPrompt(provider.label, extensionName);
const isAllowed = await this._proxy.$loginPrompt(providerData.label, extensionName);
if (!isAllowed) {
throw new Error('User did not consent to login.');
}
session = await provider.login(scopes);
session = await providerData.provider.login(scopes);
await this._proxy.$setTrustedExtensionAndAccountPreference(providerId, session.account.label, extensionId, extensionName, session.id);
} else {
await this._proxy.$requestNewSession(providerId, scopes, extensionId, extensionName);
}
}
return session;
}
async logout(providerId: string, sessionId: string): Promise<void> {
const provider = this._authenticationProviders.get(providerId);
if (!provider) {
const providerData = this._authenticationProviders.get(providerId);
if (!providerData) {
return this._proxy.$logout(providerId, sessionId);
}
return provider.logout(sessionId);
return providerData.provider.logout(sessionId);
}
registerAuthenticationProvider(provider: vscode.AuthenticationProvider): vscode.Disposable {
if (this._authenticationProviders.get(provider.id)) {
throw new Error(`An authentication provider with id '${provider.id}' is already registered.`);
registerAuthenticationProvider(id: string, label: string, provider: vscode.AuthenticationProvider, options?: vscode.AuthenticationProviderOptions): vscode.Disposable {
if (this._authenticationProviders.get(id)) {
throw new Error(`An authentication provider with id '${id}' is already registered.`);
}
this._authenticationProviders.set(provider.id, provider);
if (!this._providerIds.includes(provider.id)) {
this._providerIds.push(provider.id);
}
this._authenticationProviders.set(id, { label, provider, options: options ?? { supportsMultipleAccounts: false } });
if (!this._providers.find(p => p.id === provider.id)) {
if (!this._providers.find(p => p.id === id)) {
this._providers.push({
id: provider.id,
label: provider.label
id: id,
label: label
});
}
const listener = provider.onDidChangeSessions(e => {
this._proxy.$sendDidChangeSessions(provider.id, e);
this._proxy.$sendDidChangeSessions(id, e);
});
this._proxy.$registerAuthenticationProvider(provider.id, provider.label, provider.supportsMultipleAccounts);
this._proxy.$registerAuthenticationProvider(id, label, options?.supportsMultipleAccounts ?? false);
return new Disposable(() => {
listener.dispose();
this._authenticationProviders.delete(provider.id);
const index = this._providerIds.findIndex(id => id === provider.id);
if (index > -1) {
this._providerIds.splice(index);
}
this._authenticationProviders.delete(id);
const i = this._providers.findIndex(p => p.id === provider.id);
const i = this._providers.findIndex(p => p.id === id);
if (i > -1) {
this._providers.splice(i);
}
this._proxy.$unregisterAuthenticationProvider(provider.id);
this._proxy.$unregisterAuthenticationProvider(id);
});
}
$login(providerId: string, scopes: string[]): Promise<modes.AuthenticationSession> {
const authProvider = this._authenticationProviders.get(providerId);
if (authProvider) {
return Promise.resolve(authProvider.login(scopes));
const providerData = this._authenticationProviders.get(providerId);
if (providerData) {
return Promise.resolve(providerData.provider.login(scopes));
}
throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
}
$logout(providerId: string, sessionId: string): Promise<void> {
const authProvider = this._authenticationProviders.get(providerId);
if (authProvider) {
return Promise.resolve(authProvider.logout(sessionId));
const providerData = this._authenticationProviders.get(providerId);
if (providerData) {
return Promise.resolve(providerData.provider.logout(sessionId));
}
throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
}
$getSessions(providerId: string): Promise<ReadonlyArray<modes.AuthenticationSession>> {
const authProvider = this._authenticationProviders.get(providerId);
if (authProvider) {
return Promise.resolve(authProvider.getSessions());
const providerData = this._authenticationProviders.get(providerId);
if (providerData) {
return Promise.resolve(providerData.provider.getSessions());
}
throw new Error(`Unable to find authentication provider with handle: ${providerId}`);
}
async $getSessionAccessToken(providerId: string, sessionId: string): Promise<string> {
const authProvider = this._authenticationProviders.get(providerId);
if (authProvider) {
const sessions = await authProvider.getSessions();
const providerData = this._authenticationProviders.get(providerId);
if (providerData) {
const sessions = await providerData.provider.getSessions();
const session = sessions.find(session => session.id === sessionId);
if (session) {
return session.accessToken;
@@ -245,23 +230,4 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
this._onDidChangeAuthenticationProviders.fire({ added, removed });
return Promise.resolve();
}
async $onDidChangePassword(): Promise<void> {
this._onDidChangePassword.fire();
}
getPassword(requestingExtension: IExtensionDescription, key: string): Promise<string | undefined> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$getPassword(extensionId, key);
}
setPassword(requestingExtension: IExtensionDescription, key: string, value: string): Promise<void> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$setPassword(extensionId, key, value);
}
deletePassword(requestingExtension: IExtensionDescription, key: string): Promise<void> {
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
return this._proxy.$deletePassword(extensionId, key);
}
}

View File

@@ -103,7 +103,7 @@ export class ExtHostCommands implements ExtHostCommandsShape {
const internalArgs = apiCommand.args.map((arg, i) => {
if (!arg.validate(apiArgs[i])) {
throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', receieved: ${apiArgs[i]}`);
throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', received: ${apiArgs[i]}`);
}
return arg.convert(apiArgs[i]);
});
@@ -194,7 +194,7 @@ export class ExtHostCommands implements ExtHostCommandsShape {
}
}
private _executeContributedCommand<T>(id: string, args: any[]): Promise<T> {
private async _executeContributedCommand<T>(id: string, args: any[]): Promise<T> {
const command = this._commands.get(id);
if (!command) {
throw new Error('Unknown command');
@@ -205,17 +205,16 @@ export class ExtHostCommands implements ExtHostCommandsShape {
try {
validateConstraint(args[i], description.args[i].constraint);
} catch (err) {
return Promise.reject(new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`));
throw new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`);
}
}
}
try {
const result = callback.apply(thisArg, args);
return Promise.resolve(result);
return await callback.apply(thisArg, args);
} catch (err) {
this._logService.error(err, id);
return Promise.reject(new Error(`Running the contributed command: '${id}' failed.`));
throw new Error(`Running the contributed command: '${id}' failed.`);
}
}

View File

@@ -13,6 +13,7 @@ import * as modes from 'vs/editor/common/modes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import { ExtHostWebviews, toExtensionData } from 'vs/workbench/api/common/extHostWebview';
import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels';
import { EditorGroupColumn } from 'vs/workbench/common/editor';
@@ -178,7 +179,7 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor
options: { webviewOptions?: vscode.WebviewPanelOptions, supportsMultipleEditorsPerDocument?: boolean },
): vscode.Disposable {
const disposables = new DisposableStore();
if ('resolveCustomTextEditor' in provider) {
if (isCustomTextEditorProvider(provider)) {
disposables.add(this._editorProviders.addTextProvider(viewType, extension, provider));
this._proxy.$registerTextEditorProvider(toExtensionData(extension), viewType, options.webviewOptions || {}, {
supportsMove: !!provider.moveCustomTextEditor,
@@ -208,7 +209,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor
}));
}
async $createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken) {
const entry = this._editorProviders.get(viewType);
if (!entry) {
@@ -261,8 +261,10 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor
throw new Error(`No provider found for '${viewType}'`);
}
const viewColumn = typeConverters.ViewColumn.to(position);
const webview = this._extHostWebview.createNewWebview(handle, options, entry.extension);
const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, position, options, webview);
const panel = this._extHostWebviewPanels.createNewWebviewPanel(handle, viewType, title, viewColumn, options, webview);
const revivedResource = URI.revive(resource);
@@ -350,7 +352,6 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor
return backup.id;
}
private getCustomDocumentEntry(viewType: string, resource: UriComponents): CustomDocumentStoreEntry {
const entry = this._documents.get(viewType, URI.revive(resource));
if (!entry) {
@@ -375,6 +376,9 @@ export class ExtHostCustomEditors implements extHostProtocol.ExtHostCustomEditor
}
}
function isCustomTextEditorProvider(provider: vscode.CustomReadonlyEditorProvider<vscode.CustomDocument> | vscode.CustomTextEditorProvider): provider is vscode.CustomTextEditorProvider {
return typeof (provider as vscode.CustomTextEditorProvider).resolveCustomTextEditor === 'function';
}
function isEditEvent(e: vscode.CustomDocumentContentChangeEvent | vscode.CustomDocumentEditEvent): e is vscode.CustomDocumentEditEvent {
return typeof (e as vscode.CustomDocumentEditEvent).undo === 'function'

View File

@@ -152,7 +152,7 @@ export abstract class ExtHostDebugServiceBase implements IExtHostDebugService, E
const source = <any>src;
if (typeof source.sourceReference === 'number') {
if (typeof source.sourceReference === 'number' && source.sourceReference > 0) {
// src can be retrieved via DAP's "source" request
let debug = `debug:${encodeURIComponent(source.path || '')}`;

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import { IEditorTabDto, IExtHostEditorTabsShape } from 'vs/workbench/api/common/extHost.protocol';
import { URI } from 'vs/base/common/uri';
import { Emitter, Event } from 'vs/base/common/event';
export interface IEditorTab {
name: string;
group: number;
resource: vscode.Uri
}
export class ExtHostEditorTabs implements IExtHostEditorTabsShape {
private readonly _onDidChangeTabs = new Emitter<void>();
readonly onDidChangeTabs: Event<void> = this._onDidChangeTabs.event;
private _tabs: IEditorTab[] = [];
get tabs(): readonly IEditorTab[] {
return this._tabs;
}
$acceptEditorTabs(tabs: IEditorTabDto[]): void {
this._tabs = tabs.map(dto => {
return {
name: dto.name,
group: dto.group,
resource: URI.revive(dto.resource)
};
});
this._onDidChangeTabs.fire();
}
}

View File

@@ -5,6 +5,7 @@
import * as nls from 'vs/nls';
import * as path from 'vs/base/common/path';
import * as performance from 'vs/base/common/performance';
import { originalFSPath, joinPath } from 'vs/base/common/resources';
import { Barrier, timeout } from 'vs/base/common/async';
import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
@@ -35,6 +36,8 @@ import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelServ
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
import { Emitter, Event } from 'vs/base/common/event';
import { IExtensionActivationHost, checkActivateWorkspaceContainsExtension } from 'vs/workbench/api/common/shared/workspaceContains';
import { ExtHostSecretState, IExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState';
import { ExtensionSecrets } from 'vs/workbench/api/common/extHostSecrets';
interface ITestRunner {
/** Old test runner API, as exported from `vscode/lib/testrunner` */
@@ -50,7 +53,7 @@ export const IHostUtils = createDecorator<IHostUtils>('IHostUtils');
export interface IHostUtils {
readonly _serviceBrand: undefined;
exit(code?: number): void;
exit(code: number): void;
exists(path: string): Promise<boolean>;
realpath(path: string): Promise<string>;
}
@@ -94,6 +97,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
private readonly _readyToRunExtensions: Barrier;
protected readonly _registry: ExtensionDescriptionRegistry;
private readonly _storage: ExtHostStorage;
private readonly _secretState: ExtHostSecretState;
private readonly _storagePath: IExtensionStoragePaths;
private readonly _activator: ExtensionsActivator;
private _extensionPathIndex: Promise<TernarySearchTree<string, IExtensionDescription>> | null;
@@ -115,7 +119,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
@IExtHostInitDataService initData: IExtHostInitDataService,
@IExtensionStoragePaths storagePath: IExtensionStoragePaths,
@IExtHostTunnelService extHostTunnelService: IExtHostTunnelService,
@IExtHostTerminalService extHostTerminalService: IExtHostTerminalService
@IExtHostTerminalService extHostTerminalService: IExtHostTerminalService,
) {
super();
this._hostUtils = hostUtils;
@@ -138,10 +142,12 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
this._readyToRunExtensions = new Barrier();
this._registry = new ExtensionDescriptionRegistry(this._initData.extensions);
this._storage = new ExtHostStorage(this._extHostContext);
this._secretState = new ExtHostSecretState(this._extHostContext);
this._storagePath = storagePath;
this._instaService = instaService.createChild(new ServiceCollection(
[IExtHostStorage, this._storage]
[IExtHostStorage, this._storage],
[IExtHostSecretState, this._secretState]
));
const hostExtensions = new Set<string>();
@@ -184,6 +190,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
this._almostReadyToRunExtensions.open();
await this._extHostWorkspace.waitForInitializeCall();
performance.mark('code/extHost/ready');
this._readyToStartExtensionHost.open();
if (this._initData.autoStart) {
@@ -362,10 +369,18 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup);
return Promise.all([
<<<<<<< HEAD
this._loadCommonJSModule<IExtensionModule>(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder, !extensionDescription.browser),
=======
this._loadCommonJSModule<IExtensionModule>(extensionDescription.identifier, joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder),
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
this._loadExtensionContext(extensionDescription)
]).then(values => {
performance.mark(`code/extHost/willActivateExtension/${extensionDescription.identifier.value}`);
return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder);
}).then((activatedExtension) => {
performance.mark(`code/extHost/didActivateExtension/${extensionDescription.identifier.value}`);
return activatedExtension;
});
}
@@ -373,6 +388,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
const globalState = new ExtensionGlobalMemento(extensionDescription, this._storage);
const workspaceState = new ExtensionMemento(extensionDescription.identifier.value, false, this._storage);
const secrets = new ExtensionSecrets(extensionDescription, this._secretState);
const extensionMode = extensionDescription.isUnderDevelopment
? (this._initData.environment.extensionTestsLocationURI ? ExtensionMode.Test : ExtensionMode.Development)
: ExtensionMode.Production;
@@ -388,6 +404,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
return Object.freeze<vscode.ExtensionContext>({
globalState,
workspaceState,
secrets,
subscriptions: [],
get extensionUri() { return extensionDescription.extensionLocation; },
get extensionPath() { return extensionDescription.extensionLocation.fsPath; },
@@ -456,6 +473,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}
private _activateAllStartupFinished(): void {
// startup is considered finished
this._mainThreadExtensionsProxy.$setPerformanceMarks(performance.getMarks());
for (const desc of this._registry.getAllExtensionDescriptions()) {
if (desc.activationEvents) {
for (const activationEvent of desc.activationEvents) {
@@ -541,7 +561,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
let testRunner: ITestRunner | INewTestRunner | undefined;
let requireError: Error | undefined;
try {
testRunner = await this._loadCommonJSModule(URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false));
testRunner = await this._loadCommonJSModule(null, URI.file(extensionTestsPath), new ExtensionActivationTimesBuilder(false));
} catch (error) {
requireError = error;
}
@@ -586,11 +606,17 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}
private _testRunnerExit(code: number): void {
this._logService.info(`extension host terminating: test runner requested exit with code ${code}`);
this._logService.flush();
// wait at most 5000ms for the renderer to confirm our exit request and for the renderer socket to drain
// (this is to ensure all outstanding messages reach the renderer)
const exitPromise = this._mainThreadExtensionsProxy.$onExtensionHostExit(code);
const drainPromise = this._extHostContext.drain();
Promise.race([Promise.all([exitPromise, drainPromise]), timeout(5000)]).then(() => {
this._logService.info(`exiting with code ${code}`);
this._logService.flush();
this._hostUtils.exit(code);
});
}
@@ -644,7 +670,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}
try {
performance.mark(`code/extHost/willResolveAuthority/${authorityPrefix}`);
const result = await resolver.resolve(remoteAuthority, { resolveAttempt });
performance.mark(`code/extHost/didResolveAuthorityOK/${authorityPrefix}`);
this._disposables.add(await this._extHostTunnelService.setTunnelExtensionFunctions(resolver));
// Split merged API result into separate authority/options
@@ -667,6 +695,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}
};
} catch (err) {
performance.mark(`code/extHost/didResolveAuthorityError/${authorityPrefix}`);
if (err instanceof RemoteAuthorityResolverError) {
return {
type: 'error',
@@ -754,7 +783,11 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
protected abstract _beforeAlmostReadyToRunExtensions(): Promise<void>;
protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined;
<<<<<<< HEAD
protected abstract _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder, isRemote?: boolean): Promise<T>;
=======
protected abstract _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>;
>>>>>>> 89b6e0164fa770333755b11504e19a4232b1a2d4
public abstract $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>;
}

View File

@@ -8,7 +8,7 @@ import { IRelativePattern, parse } from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import type * as vscode from 'vscode';
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, MainContext, SourceTargetPair, IWorkspaceEditDto, MainThreadBulkEditsShape } from './extHost.protocol';
import { ExtHostFileSystemEventServiceShape, FileSystemEvents, IMainContext, SourceTargetPair, IWorkspaceEditDto, IWillRunFileOperationParticipation } from './extHost.protocol';
import * as typeConverter from './extHostTypeConverters';
import { Disposable, WorkspaceEdit } from './extHostTypes';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
@@ -122,8 +122,7 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
constructor(
mainContext: IMainContext,
private readonly _logService: ILogService,
private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors,
private readonly _mainThreadBulkEdits: MainThreadBulkEditsShape = mainContext.getProxy(MainContext.MainThreadBulkEdits)
private readonly _extHostDocumentsAndEditors: ExtHostDocumentsAndEditors
) {
//
}
@@ -178,24 +177,21 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
};
}
async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise<any> {
async $onWillRunFileOperation(operation: FileOperation, files: SourceTargetPair[], timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {
switch (operation) {
case FileOperation.MOVE:
await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, undoRedoGroupId, timeout, token);
break;
return await this._fireWillEvent(this._onWillRenameFile, { files: files.map(f => ({ oldUri: URI.revive(f.source!), newUri: URI.revive(f.target) })) }, timeout, token);
case FileOperation.DELETE:
await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token);
break;
return await this._fireWillEvent(this._onWillDeleteFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
case FileOperation.CREATE:
await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, undoRedoGroupId, timeout, token);
break;
default:
//ignore, dont send
return await this._fireWillEvent(this._onWillCreateFile, { files: files.map(f => URI.revive(f.target)) }, timeout, token);
}
return undefined;
}
private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: Omit<E, 'waitUntil'>, undoRedoGroupId: number | undefined, timeout: number, token: CancellationToken): Promise<any> {
private async _fireWillEvent<E extends IWaitUntil>(emitter: AsyncEmitter<E>, data: Omit<E, 'waitUntil'>, timeout: number, token: CancellationToken): Promise<IWillRunFileOperationParticipation | undefined> {
const extensionNames = new Set<string>();
const edits: WorkspaceEdit[] = [];
await emitter.fireAsync(data, token, async (thenable, listener) => {
@@ -204,25 +200,28 @@ export class ExtHostFileSystemEventService implements ExtHostFileSystemEventServ
const result = await Promise.resolve(thenable);
if (result instanceof WorkspaceEdit) {
edits.push(result);
extensionNames.add((<IExtensionListener<E>>listener).extension.displayName ?? (<IExtensionListener<E>>listener).extension.identifier.value);
}
if (Date.now() - now > timeout) {
this._logService.warn('SLOW file-participant', (<IExtensionListener<E>>listener).extension?.identifier);
this._logService.warn('SLOW file-participant', (<IExtensionListener<E>>listener).extension.identifier);
}
});
if (token.isCancellationRequested) {
return;
return undefined;
}
if (edits.length > 0) {
// concat all WorkspaceEdits collected via waitUntil-call and apply them in one go.
const dto: IWorkspaceEditDto = { edits: [] };
for (let edit of edits) {
let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors);
dto.edits = dto.edits.concat(edits);
}
return this._mainThreadBulkEdits.$tryApplyWorkspaceEdit(dto, undoRedoGroupId);
if (edits.length === 0) {
return undefined;
}
// concat all WorkspaceEdits collected via waitUntil-call and send them over to the renderer
const dto: IWorkspaceEditDto = { edits: [] };
for (let edit of edits) {
let { edits } = typeConverter.WorkspaceEdit.from(edit, this._extHostDocumentsAndEditors);
dto.edits = dto.edits.concat(edits);
}
return { edit: dto, extensionNames: Array.from(extensionNames) };
}
}

View File

@@ -27,11 +27,12 @@ import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensio
import { IURITransformer } from 'vs/base/common/uriIpc';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { VSBuffer } from 'vs/base/common/buffer';
import { encodeSemanticTokensDto } from 'vs/workbench/api/common/shared/semanticTokensDto';
import { encodeSemanticTokensDto } from 'vs/editor/common/services/semanticTokensDto';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { Cache } from './cache';
import { StopWatch } from 'vs/base/common/stopwatch';
import { CancellationError } from 'vs/base/common/errors';
// --- adapter
@@ -1062,6 +1063,20 @@ class SignatureHelpAdapter {
}
}
class InlineHintsAdapter {
constructor(
private readonly _documents: ExtHostDocuments,
private readonly _provider: vscode.InlineHintsProvider,
) { }
provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise<extHostProtocol.IInlineHintsDto | undefined> {
const doc = this._documents.getDocument(resource);
return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => {
return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined;
});
}
}
class LinkProviderAdapter {
private _cache = new Cache<vscode.DocumentLink>('DocumentLink');
@@ -1320,7 +1335,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov
| SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter
| TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter
| SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter
| LinkedEditingRangeAdapter;
| LinkedEditingRangeAdapter | InlineHintsAdapter;
class AdapterData {
constructor(
@@ -1403,7 +1418,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
return ExtHostLanguageFeatures._handlePool++;
}
private _withAdapter<A, R>(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise<R>, fallbackValue: R): Promise<R> {
private _withAdapter<A, R>(handle: number, ctor: { new(...args: any[]): A; }, callback: (adapter: A, extension: IExtensionDescription | undefined) => Promise<R>, fallbackValue: R, allowCancellationError: boolean = false): Promise<R> {
const data = this._adapter.get(handle);
if (!data) {
return Promise.resolve(fallbackValue);
@@ -1421,8 +1436,11 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
Promise.resolve(p).then(
() => this._logService.trace(`[${extension.identifier.value}] provider DONE after ${Date.now() - t1}ms`),
err => {
this._logService.error(`[${extension.identifier.value}] provider FAILED`);
this._logService.error(err);
const isExpectedError = allowCancellationError && (err instanceof CancellationError);
if (!isExpectedError) {
this._logService.error(`[${extension.identifier.value}] provider FAILED`);
this._logService.error(err);
}
}
);
}
@@ -1711,7 +1729,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
}
$provideDocumentSemanticTokens(handle: number, resource: UriComponents, previousResultId: number, token: CancellationToken): Promise<VSBuffer | null> {
return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null);
return this._withAdapter(handle, DocumentSemanticTokensAdapter, adapter => adapter.provideDocumentSemanticTokens(URI.revive(resource), previousResultId, token), null, true);
}
$releaseDocumentSemanticTokens(handle: number, semanticColoringResultId: number): void {
@@ -1725,7 +1743,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
}
$provideDocumentRangeSemanticTokens(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise<VSBuffer | null> {
return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null);
return this._withAdapter(handle, DocumentRangeSemanticTokensAdapter, adapter => adapter.provideDocumentRangeSemanticTokens(URI.revive(resource), range, token), null, true);
}
//#endregion
@@ -1770,6 +1788,27 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.releaseSignatureHelp(id), undefined);
}
// --- inline hints
registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable {
const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined;
const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension);
this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle);
let result = this._createDisposable(handle);
if (eventHandle !== undefined) {
const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle));
result = Disposable.from(result, subscription);
}
return result;
}
$provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise<extHostProtocol.IInlineHintsDto | undefined> {
return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined);
}
// --- links
registerDocumentLinkProvider(extension: IExtensionDescription | undefined, selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable {
@@ -1882,7 +1921,7 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF
return {
beforeText: ExtHostLanguageFeatures._serializeRegExp(onEnterRule.beforeText),
afterText: onEnterRule.afterText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.afterText) : undefined,
oneLineAboveText: onEnterRule.oneLineAboveText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.oneLineAboveText) : undefined,
previousLineText: onEnterRule.previousLineText ? ExtHostLanguageFeatures._serializeRegExp(onEnterRule.previousLineText) : undefined,
action: onEnterRule.action
};
}

View File

@@ -8,6 +8,7 @@ import type * as vscode from 'vscode';
import { MainContext, MainThreadMessageServiceShape, MainThreadMessageOptions, IMainContext } from './extHost.protocol';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
function isMessageItem(item: any): item is vscode.MessageItem {
return item && item.title;
@@ -37,9 +38,14 @@ export class ExtHostMessageService {
items = [optionsOrFirstItem, ...rest];
} else {
options.modal = optionsOrFirstItem && optionsOrFirstItem.modal;
options.useCustom = optionsOrFirstItem && optionsOrFirstItem.useCustom;
items = rest;
}
if (options.useCustom) {
checkProposedApiEnabled(extension);
}
const commands: { title: string; isCloseAffordance: boolean; handle: number; }[] = [];
for (let handle = 0; handle < items.length; handle++) {

View File

@@ -78,8 +78,8 @@ export interface ExtHostNotebookOutputRenderingHandler {
}
export class ExtHostNotebookKernelProviderAdapter extends Disposable {
private _kernelToId = new Map<vscode.NotebookKernel, string>();
private _idToKernel = new Map<string, vscode.NotebookKernel>();
private _kernelToFriendlyId = new Map<vscode.NotebookKernel, string>();
private _friendlyIdToKernel = new Map<string, vscode.NotebookKernel>();
constructor(
private readonly _proxy: MainThreadNotebookShape,
private readonly _handle: number,
@@ -101,24 +101,25 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable {
const newMap = new Map<vscode.NotebookKernel, string>();
let kernel_unique_pool = 0;
const kernelIdCache = new Set<string>();
const kernelFriendlyIdCache = new Set<string>();
const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => {
let id = this._kernelToId.get(kernel);
if (id === undefined) {
if (kernel.id && kernelIdCache.has(kernel.id)) {
id = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`;
let friendlyId = this._kernelToFriendlyId.get(kernel);
if (friendlyId === undefined) {
if (kernel.id && kernelFriendlyIdCache.has(kernel.id)) {
friendlyId = `${this._extension.identifier.value}_${kernel.id}_${kernel_unique_pool++}`;
} else {
id = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`;
friendlyId = `${this._extension.identifier.value}_${kernel.id || UUID.generateUuid()}`;
}
this._kernelToId.set(kernel, id);
this._kernelToFriendlyId.set(kernel, friendlyId);
}
newMap.set(kernel, id);
newMap.set(kernel, friendlyId);
return {
id,
id: kernel.id,
friendlyId: friendlyId,
label: kernel.label,
extension: this._extension.identifier,
extensionLocation: this._extension.extensionLocation,
@@ -129,22 +130,22 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable {
};
});
this._kernelToId = newMap;
this._kernelToFriendlyId = newMap;
this._idToKernel.clear();
this._kernelToId.forEach((value, key) => {
this._idToKernel.set(value, key);
this._friendlyIdToKernel.clear();
this._kernelToFriendlyId.forEach((value, key) => {
this._friendlyIdToKernel.set(value, key);
});
return transformedData;
}
getKernel(kernelId: string) {
return this._idToKernel.get(kernelId);
getKernelByFriendlyId(kernelId: string) {
return this._friendlyIdToKernel.get(kernelId);
}
async resolveNotebook(kernelId: string, document: ExtHostNotebookDocument, webview: vscode.NotebookCommunication, token: CancellationToken) {
const kernel = this._idToKernel.get(kernelId);
const kernel = this._friendlyIdToKernel.get(kernelId);
if (kernel && this._provider.resolveKernel) {
return this._provider.resolveKernel(kernel, document.notebookDocument, webview, token);
@@ -152,7 +153,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable {
}
async executeNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) {
const kernel = this._idToKernel.get(kernelId);
const kernel = this._friendlyIdToKernel.get(kernelId);
if (!kernel) {
return;
@@ -166,7 +167,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable {
}
async cancelNotebook(kernelId: string, document: ExtHostNotebookDocument, cell: ExtHostCell | undefined) {
const kernel = this._idToKernel.get(kernelId);
const kernel = this._friendlyIdToKernel.get(kernelId);
if (!kernel) {
return;
@@ -580,10 +581,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
this._outputDisplayOrder = displayOrder;
}
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) {
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelFriendlyId: string | undefined; }) {
if (event.providerHandle !== undefined) {
this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => {
const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined;
const kernel = event.kernelFriendlyId ? adapter.getKernelByFriendlyId(event.kernelFriendlyId) : undefined;
this._editors.forEach(editor => {
if (editor.editor.notebookData === document) {
editor.editor._acceptKernel(kernel);

View File

@@ -113,14 +113,17 @@ export class ExtHostCell extends Disposable {
get cell(): vscode.NotebookCell {
if (!this._cell) {
const that = this;
const document = this._extHostDocument.getDocument(this.uri)!.document;
const data = this._extHostDocument.getDocument(this.uri);
if (!data) {
throw new Error(`MISSING extHostDocument for notebook cell: ${this.uri}`);
}
this._cell = Object.freeze({
get index() { return that._notebook.getCellIndex(that); },
notebook: that._notebook.notebookDocument,
uri: that.uri,
cellKind: this._cellData.cellKind,
document,
get language() { return document.languageId; },
document: data.document,
get language() { return data!.document.languageId; },
get outputs() { return that._outputs; },
set outputs(value) { that._updateOutputs(value); },
get metadata() { return that._metadata; },

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as performance from 'vs/base/common/performance';
import { TernarySearchTree } from 'vs/base/common/map';
import { URI } from 'vs/base/common/uri';
import { MainThreadTelemetryShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
@@ -52,7 +53,9 @@ export abstract class RequireInterceptor {
this._installInterceptor();
performance.mark('code/extHost/willWaitForConfig');
const configProvider = await this._extHostConfiguration.getConfigProvider();
performance.mark('code/extHost/didWaitForConfig');
const extensionPaths = await this._extHostExtensionService.getExtensionPathIndex();
this.register(new VSCodeNodeModuleFactory(this._apiFactory, extensionPaths, this._extensionRegistry, configProvider, this._logService));

View File

@@ -0,0 +1,43 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import { ExtHostSecretState } from 'vs/workbench/api/common/exHostSecretState';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { Emitter, Event } from 'vs/base/common/event';
export class ExtensionSecrets implements vscode.SecretStorage {
protected readonly _id: string;
readonly #secretState: ExtHostSecretState;
private _onDidChange = new Emitter<vscode.SecretStorageChangeEvent>();
readonly onDidChange: Event<vscode.SecretStorageChangeEvent> = this._onDidChange.event;
constructor(extensionDescription: IExtensionDescription, secretState: ExtHostSecretState) {
this._id = ExtensionIdentifier.toKey(extensionDescription.identifier);
this.#secretState = secretState;
this.#secretState.onDidChangePassword(e => {
if (e.extensionId === this._id) {
this._onDidChange.fire({ key: e.key });
}
});
}
get(key: string): Promise<string | undefined> {
return this.#secretState.get(this._id, key);
}
store(key: string, value: string): Promise<void> {
return this.#secretState.store(this._id, key, value);
}
delete(key: string): Promise<void> {
return this.#secretState.delete(this._id, key);
}
}

View File

@@ -10,8 +10,6 @@ import { MainContext, MainThreadStatusBarShape, IMainContext, ICommandDto } from
import { localize } from 'vs/nls';
import { CommandsConverter } from 'vs/workbench/api/common/extHostCommands';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
export class ExtHostStatusBarEntry implements vscode.StatusBarItem {
private static ID_GEN = 0;
@@ -48,9 +46,8 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem {
private _proxy: MainThreadStatusBarShape;
private _commands: CommandsConverter;
private _accessibilityInformation?: vscode.AccessibilityInformation;
private _extension?: IExtensionDescription;
constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription) {
constructor(proxy: MainThreadStatusBarShape, commands: CommandsConverter, id: string, name: string, alignment: ExtHostStatusBarAlignment = ExtHostStatusBarAlignment.Left, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation) {
this._id = ExtHostStatusBarEntry.ID_GEN++;
this._proxy = proxy;
this._commands = commands;
@@ -59,7 +56,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem {
this._alignment = alignment;
this._priority = priority;
this._accessibilityInformation = accessibilityInformation;
this._extension = extension;
}
public get id(): number {
@@ -87,10 +83,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem {
}
public get backgroundColor(): ThemeColor | undefined {
if (this._extension) {
checkProposedApiEnabled(this._extension);
}
return this._backgroundColor;
}
@@ -118,10 +110,6 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem {
}
public set backgroundColor(color: ThemeColor | undefined) {
if (this._extension) {
checkProposedApiEnabled(this._extension);
}
if (color && !ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.has(color.id)) {
color = undefined;
}
@@ -248,8 +236,8 @@ export class ExtHostStatusBar {
this._statusMessage = new StatusBarMessage(this);
}
createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation, extension?: IExtensionDescription): vscode.StatusBarItem {
return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation, extension);
createStatusBarEntry(id: string, name: string, alignment?: ExtHostStatusBarAlignment, priority?: number, accessibilityInformation?: vscode.AccessibilityInformation): vscode.StatusBarItem {
return new ExtHostStatusBarEntry(this._proxy, this._commands, id, name, alignment, priority, accessibilityInformation);
}
setStatusBarMessage(text: string, timeoutOrThenable?: number | Thenable<any>): Disposable {

View File

@@ -5,12 +5,11 @@
import type * as vscode from 'vscode';
import { Event, Emitter } from 'vs/base/common/event';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostTerminalServiceShape, MainContext, MainThreadTerminalServiceShape, IShellLaunchConfigDto, IShellDefinitionDto, IShellAndArgsDto, ITerminalDimensionsDto, ITerminalLinkDto, TerminalIdentifier } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostConfigProvider } from 'vs/workbench/api/common/extHostConfiguration';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ITerminalChildProcess, EXT_HOST_CREATION_DELAY, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal';
import { timeout } from 'vs/base/common/async';
import { ITerminalChildProcess, ITerminalLaunchError, ITerminalDimensionsOverride } from 'vs/workbench/contrib/terminal/common/terminal';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
import { IDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
@@ -21,6 +20,7 @@ import { localize } from 'vs/nls';
import { NotSupportedError } from 'vs/base/common/errors';
import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { generateUuid } from 'vs/base/common/uuid';
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable {
@@ -47,63 +47,8 @@ export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, ID
export const IExtHostTerminalService = createDecorator<IExtHostTerminalService>('IExtHostTerminalService');
export class BaseExtHostTerminal {
public _id: number | undefined;
protected _idPromise: Promise<number>;
private _idPromiseComplete: ((value: number) => any) | undefined;
export class ExtHostTerminal implements vscode.Terminal {
private _disposed: boolean = false;
private _queuedRequests: ApiRequest[] = [];
constructor(
protected _proxy: MainThreadTerminalServiceShape,
id?: number
) {
this._idPromise = new Promise<number>(c => {
if (id !== undefined) {
this._id = id;
c(id);
} else {
this._idPromiseComplete = c;
}
});
}
public dispose(): void {
if (!this._disposed) {
this._disposed = true;
this._queueApiRequest(this._proxy.$dispose, []);
}
}
protected _checkDisposed() {
if (this._disposed) {
throw new Error('Terminal has already been disposed');
}
}
protected _queueApiRequest(callback: (...args: any[]) => void, args: any[]): void {
const request: ApiRequest = new ApiRequest(callback, args);
if (!this._id) {
this._queuedRequests.push(request);
return;
}
request.run(this._proxy, this._id);
}
public _runQueuedRequests(id: number): void {
this._id = id;
if (this._idPromiseComplete) {
this._idPromiseComplete(id);
this._idPromiseComplete = undefined;
}
this._queuedRequests.forEach((r) => {
r.run(this._proxy, id);
});
this._queuedRequests.length = 0;
}
}
export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Terminal {
private _pidPromise: Promise<number | undefined>;
private _cols: number | undefined;
private _pidPromiseComplete: ((value: number | undefined) => any) | undefined;
@@ -113,12 +58,11 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi
public isOpen: boolean = false;
constructor(
proxy: MainThreadTerminalServiceShape,
private _proxy: MainThreadTerminalServiceShape,
public _id: TerminalIdentifier,
private readonly _creationOptions: vscode.TerminalOptions | vscode.ExtensionTerminalOptions,
private _name?: string,
id?: number
) {
super(proxy, id);
this._creationOptions = Object.freeze(this._creationOptions);
this._pidPromise = new Promise<number | undefined>(c => this._pidPromiseComplete = c);
}
@@ -133,16 +77,35 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi
hideFromUser?: boolean,
isFeatureTerminal?: boolean
): Promise<void> {
const result = await this._proxy.$createTerminal({ name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal });
this._name = result.name;
this._runQueuedRequests(result.id);
if (typeof this._id !== 'string') {
throw new Error('Terminal has already been created');
}
await this._proxy.$createTerminal(this._id, { name: this._name, shellPath, shellArgs, cwd, env, waitOnExit, strictEnv, hideFromUser, isFeatureTerminal });
}
public async createExtensionTerminal(): Promise<number> {
const result = await this._proxy.$createTerminal({ name: this._name, isExtensionTerminal: true });
this._name = result.name;
this._runQueuedRequests(result.id);
return result.id;
if (typeof this._id !== 'string') {
throw new Error('Terminal has already been created');
}
await this._proxy.$createTerminal(this._id, { name: this._name, isExtensionTerminal: true });
// At this point, the id has been set via `$acceptTerminalOpened`
if (typeof this._id === 'string') {
throw new Error('Terminal creation failed');
}
return this._id;
}
public dispose(): void {
if (!this._disposed) {
this._disposed = true;
this._proxy.$dispose(this._id);
}
}
private _checkDisposed() {
if (this._disposed) {
throw new Error('Terminal has already been disposed');
}
}
public get name(): string {
@@ -194,17 +157,17 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi
public sendText(text: string, addNewLine: boolean = true): void {
this._checkDisposed();
this._queueApiRequest(this._proxy.$sendText, [text, addNewLine]);
this._proxy.$sendText(this._id, text, addNewLine);
}
public show(preserveFocus: boolean): void {
this._checkDisposed();
this._queueApiRequest(this._proxy.$show, [preserveFocus]);
this._proxy.$show(this._id, preserveFocus);
}
public hide(): void {
this._checkDisposed();
this._queueApiRequest(this._proxy.$hide, []);
this._proxy.$hide(this._id);
}
public _setProcessId(processId: number | undefined): void {
@@ -223,20 +186,6 @@ export class ExtHostTerminal extends BaseExtHostTerminal implements vscode.Termi
}
}
class ApiRequest {
private _callback: (...args: any[]) => void;
private _args: any[];
constructor(callback: (...args: any[]) => void, args: any[]) {
this._callback = callback;
this._args = args;
}
public run(proxy: MainThreadTerminalServiceShape, id: number) {
this._callback.apply(proxy, [id].concat(this._args));
}
}
export class ExtHostPseudoterminal implements ITerminalChildProcess {
private readonly _onProcessData = new Emitter<string>();
public readonly onProcessData: Event<string> = this._onProcessData.event;
@@ -271,6 +220,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess {
}
}
acknowledgeDataEvent(charCount: number): void {
// No-op, flow control is not supported in extension owned terminals. If this is ever
// implemented it will need new pause and resume VS Code APIs.
}
getInitialCwd(): Promise<string> {
return Promise.resolve('');
}
@@ -296,6 +250,11 @@ export class ExtHostPseudoterminal implements ITerminalChildProcess {
}
this._pty.open(initialDimensions ? initialDimensions : undefined);
if (this._pty.setDimensions && initialDimensions) {
this._pty.setDimensions(initialDimensions);
}
this._onProcessReady.fire({ pid: -1, cwd: '' });
}
}
@@ -370,7 +329,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
public abstract $acceptWorkspacePermissionsChanged(isAllowed: boolean): void;
public createExtensionTerminal(options: vscode.ExtensionTerminalOptions): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, options, options.name);
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name);
const p = new ExtHostPseudoterminal(options.pty);
terminal.createExtensionTerminal().then(id => {
const disposable = this._setupExtHostProcessListeners(id, p);
@@ -381,7 +340,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
}
public attachPtyToTerminal(id: number, pty: vscode.Pseudoterminal): void {
const terminal = this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (!terminal) {
throw new Error(`Cannot resolve terminal with id ${id} for virtual process`);
}
@@ -399,7 +358,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
}
return;
}
const terminal = await this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (terminal) {
this._activeTerminal = terminal;
if (original !== this._activeTerminal) {
@@ -409,14 +368,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
}
public async $acceptTerminalProcessData(id: number, data: string): Promise<void> {
const terminal = await this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (terminal) {
this._onDidWriteTerminalData.fire({ terminal, data });
}
}
public async $acceptTerminalDimensions(id: number, cols: number, rows: number): Promise<void> {
const terminal = await this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (terminal) {
if (terminal.setDimensions(cols, rows)) {
this._onDidChangeTerminalDimensions.fire({
@@ -428,23 +387,19 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
}
public async $acceptTerminalMaximumDimensions(id: number, cols: number, rows: number): Promise<void> {
await this._getTerminalByIdEventually(id);
// Extension pty terminal only - when virtual process resize fires it means that the
// terminal's maximum dimensions changed
this._terminalProcesses.get(id)?.resize(cols, rows);
}
public async $acceptTerminalTitleChange(id: number, name: string): Promise<void> {
await this._getTerminalByIdEventually(id);
const extHostTerminal = this._getTerminalObjectById(this.terminals, id);
if (extHostTerminal) {
extHostTerminal.name = name;
const terminal = this._getTerminalById(id);
if (terminal) {
terminal.name = name;
}
}
public async $acceptTerminalClosed(id: number, exitCode: number | undefined): Promise<void> {
await this._getTerminalByIdEventually(id);
const index = this._getTerminalObjectIndexById(this.terminals, id);
if (index !== null) {
const terminal = this._terminals.splice(index, 1)[0];
@@ -453,13 +408,17 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
}
}
public $acceptTerminalOpened(id: number, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void {
const index = this._getTerminalObjectIndexById(this._terminals, id);
if (index !== null) {
// The terminal has already been created (via createTerminal*), only fire the event
this._onDidOpenTerminal.fire(this.terminals[index]);
this.terminals[index].isOpen = true;
return;
public $acceptTerminalOpened(id: number, extHostTerminalId: string | undefined, name: string, shellLaunchConfigDto: IShellLaunchConfigDto): void {
if (extHostTerminalId) {
// Resolve with the renderer generated id
const index = this._getTerminalObjectIndexById(this._terminals, extHostTerminalId);
if (index !== null) {
// The terminal has already been created (via createTerminal*), only fire the event
this.terminals[index]._id = id;
this._onDidOpenTerminal.fire(this.terminals[index]);
this.terminals[index].isOpen = true;
return;
}
}
const creationOptions: vscode.TerminalOptions = {
@@ -470,14 +429,14 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
env: shellLaunchConfigDto.env,
hideFromUser: shellLaunchConfigDto.hideFromUser
};
const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id);
const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name);
this._terminals.push(terminal);
this._onDidOpenTerminal.fire(terminal);
terminal.isOpen = true;
}
public async $acceptTerminalProcessId(id: number, processId: number): Promise<void> {
const terminal = await this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (terminal) {
terminal._setProcessId(processId);
}
@@ -486,7 +445,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
public async $startExtensionTerminal(id: number, initialDimensions: ITerminalDimensionsDto | undefined): Promise<ITerminalLaunchError | undefined> {
// Make sure the ExtHostTerminal exists so onDidOpenTerminal has fired before we call
// Pseudoterminal.start
const terminal = await this._getTerminalByIdEventually(id);
const terminal = this._getTerminalById(id);
if (!terminal) {
return { message: localize('launchFail.idMissingOnExtHost', "Could not find the terminal with id {0} on the extension host", id) };
}
@@ -539,6 +498,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
return disposables;
}
public $acceptProcessAckDataEvent(id: number, charCount: number): void {
this._terminalProcesses.get(id)?.acknowledgeDataEvent(charCount);
}
public $acceptProcessInput(id: number, data: string): void {
this._terminalProcesses.get(id)?.input(data);
}
@@ -670,32 +633,6 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
this._proxy.$sendProcessExit(id, exitCode);
}
// TODO: This could be improved by using a single promise and resolve it when the terminal is ready
private _getTerminalByIdEventually(id: number, retries: number = 5): Promise<ExtHostTerminal | undefined> {
if (!this._getTerminalPromises[id]) {
this._getTerminalPromises[id] = this._createGetTerminalPromise(id, retries);
}
return this._getTerminalPromises[id];
}
private _createGetTerminalPromise(id: number, retries: number = 5): Promise<ExtHostTerminal | undefined> {
return new Promise(c => {
if (retries === 0) {
c(undefined);
return;
}
const terminal = this._getTerminalById(id);
if (terminal) {
c(terminal);
} else {
// This should only be needed immediately after createTerminalRenderer is called as
// the ExtHostTerminal has not yet been iniitalized
timeout(EXT_HOST_CREATION_DELAY * 2).then(() => c(this._createGetTerminalPromise(id, retries - 1)));
}
});
}
private _getTerminalById(id: number): ExtHostTerminal | null {
return this._getTerminalObjectById(this._terminals, id);
}
@@ -705,7 +642,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I
return index !== null ? array[index] : null;
}
private _getTerminalObjectIndexById<T extends ExtHostTerminal>(array: T[], id: number): number | null {
private _getTerminalObjectIndexById<T extends ExtHostTerminal>(array: T[], id: TerminalIdentifier): number | null {
let index: number | null = null;
array.some((item, i) => {
const thisId = item._id;

View File

@@ -6,7 +6,6 @@
import { mapFind } from 'vs/base/common/arrays';
import { disposableTimeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { throttle } from 'vs/base/common/decorators';
import { Emitter } from 'vs/base/common/event';
import { once } from 'vs/base/common/functional';
import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
@@ -14,25 +13,35 @@ import { isDefined } from 'vs/base/common/types';
import { URI, UriComponents } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { ExtHostTestingResource, ExtHostTestingShape, MainContext, MainThreadTestingShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { TestItem } from 'vs/workbench/api/common/extHostTypeConverters';
import { Disposable, RequiredTestItem } from 'vs/workbench/api/common/extHostTypes';
import { Disposable } from 'vs/workbench/api/common/extHostTypes';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import { OwnedTestCollection, SingleUseTestCollection } from 'vs/workbench/contrib/testing/common/ownedTestCollection';
import { AbstractIncrementalTestCollection, EMPTY_TEST_RESULT, IncrementalChangeCollector, IncrementalTestCollectionItem, InternalTestItem, InternalTestItemWithChildren, InternalTestResults, RunTestForProviderRequest, RunTestsResult, TestDiffOpType, TestIdWithProvider, TestsDiff } from 'vs/workbench/contrib/testing/common/testCollection';
import type * as vscode from 'vscode';
const getTestSubscriptionKey = (resource: ExtHostTestingResource, uri: URI) => `${resource}:${uri.toString()}`;
export class ExtHostTesting implements ExtHostTestingShape {
private readonly resultsChangedEmitter = new Emitter<void>();
private readonly providers = new Map<string, vscode.TestProvider>();
private readonly proxy: MainThreadTestingShape;
private readonly ownedTests = new OwnedTestCollection();
private readonly testSubscriptions = new Map<string, { collection: SingleUseTestCollection, store: IDisposable }>();
private readonly testSubscriptions = new Map<string, {
collection: SingleUseTestCollection;
store: IDisposable;
subscribeFn: (id: string, provider: vscode.TestProvider) => void;
}>();
private workspaceObservers: WorkspaceFolderTestObserverFactory;
private textDocumentObservers: TextDocumentTestObserverFactory;
public onLastResultsChanged = this.resultsChangedEmitter.event;
public lastResults?: vscode.TestResults;
constructor(@IExtHostRpcService rpc: IExtHostRpcService, @IExtHostDocumentsAndEditors private readonly documents: IExtHostDocumentsAndEditors, @IExtHostWorkspace private readonly workspace: IExtHostWorkspace) {
this.proxy = rpc.getProxy(MainContext.MainThreadTesting);
this.workspaceObservers = new WorkspaceFolderTestObserverFactory(this.proxy);
@@ -47,6 +56,14 @@ export class ExtHostTesting implements ExtHostTestingShape {
this.providers.set(providerId, provider);
this.proxy.$registerTestProvider(providerId);
// give the ext a moment to register things rather than synchronously invoking within activate()
const toSubscribe = [...this.testSubscriptions.keys()];
setTimeout(() => {
for (const subscription of toSubscribe) {
this.testSubscriptions.get(subscription)?.subscribeFn(providerId, provider);
}
}, 0);
return new Disposable(() => {
this.providers.delete(providerId);
this.proxy.$unregisterTestProvider(providerId);
@@ -70,7 +87,7 @@ export class ExtHostTesting implements ExtHostTestingShape {
/**
* Implements vscode.test.runTests
*/
public async runTests(req: vscode.TestRunOptions<vscode.TestItem>) {
public async runTests(req: vscode.TestRunOptions<vscode.TestItem>, token = CancellationToken.None) {
await this.proxy.$runTests({
tests: req.tests
// Find workspace items first, then owned tests, then document tests.
@@ -82,14 +99,27 @@ export class ExtHostTesting implements ExtHostTestingShape {
.filter(isDefined)
.map(item => ({ providerId: item.providerId, testId: item.id })),
debug: req.debug
});
}, token);
}
/**
* Updates test results shown to extensions.
* @override
*/
public $publishTestResults(results: InternalTestResults): void {
const convert = (item: InternalTestItemWithChildren): vscode.RequiredTestItem =>
({ ...TestItem.toShallow(item.item), children: item.children.map(convert) });
this.lastResults = { tests: results.tests.map(convert) };
this.resultsChangedEmitter.fire();
}
/**
* Handles a request to read tests for a file, or workspace.
* @override
*/
public $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) {
public async $subscribeToTests(resource: ExtHostTestingResource, uriComponents: UriComponents) {
const uri = URI.revive(uriComponents);
const subscriptionKey = getTestSubscriptionKey(resource, uri);
if (this.testSubscriptions.has(subscriptionKey)) {
@@ -98,12 +128,29 @@ export class ExtHostTesting implements ExtHostTestingShape {
let method: undefined | ((p: vscode.TestProvider) => vscode.TestHierarchy<vscode.TestItem> | undefined);
if (resource === ExtHostTestingResource.TextDocument) {
const document = this.documents.getDocument(uri);
let document = this.documents.getDocument(uri);
// we can ask to subscribe to tests before the documents are populated in
// the extension host. Try to wait.
if (!document) {
const store = new DisposableStore();
document = await new Promise<ExtHostDocumentData | undefined>(resolve => {
store.add(disposableTimeout(() => resolve(undefined), 5000));
store.add(this.documents.onDidAddDocuments(e => {
const data = e.find(data => data.document.uri.toString() === uri.toString());
if (data) { resolve(data); }
}));
}).finally(() => store.dispose());
}
if (document) {
method = p => p.createDocumentTestHierarchy?.(document.document);
const folder = await this.workspace.getWorkspaceFolder2(uri, false);
method = p => p.createDocumentTestHierarchy
? p.createDocumentTestHierarchy(document!.document)
: this.createDefaultDocumentTestHierarchy(p, document!.document, folder);
}
} else {
const folder = this.workspace.getWorkspaceFolder(uri, false);
const folder = await this.workspace.getWorkspaceFolder2(uri, false);
if (folder) {
method = p => p.createWorkspaceTestHierarchy?.(folder);
}
@@ -113,24 +160,34 @@ export class ExtHostTesting implements ExtHostTestingShape {
return;
}
const disposable = new DisposableStore();
const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff)));
for (const [id, provider] of this.providers) {
const subscribeFn = (id: string, provider: vscode.TestProvider) => {
try {
const hierarchy = method(provider);
const hierarchy = method!(provider);
if (!hierarchy) {
continue;
return;
}
collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, 1]);
disposable.add(hierarchy);
collection.addRoot(hierarchy.root, id);
Promise.resolve(hierarchy.discoveredInitialTests).then(() => collection.pushDiff([TestDiffOpType.DeltaDiscoverComplete, -1]));
hierarchy.onDidChangeTest(e => collection.onItemChange(e, id));
} catch (e) {
console.error(e);
}
};
const disposable = new DisposableStore();
const collection = disposable.add(this.ownedTests.createForHierarchy(diff => this.proxy.$publishDiff(resource, uriComponents, diff)));
for (const [id, provider] of this.providers) {
subscribeFn(id, provider);
}
this.testSubscriptions.set(subscriptionKey, { store: disposable, collection });
// note: we don't increment the root count initially -- this is done by the
// main thread, incrementing once per extension host. We just push the
// diff to signal that roots have been discovered.
collection.pushDiff([TestDiffOpType.DeltaRootsComplete, -1]);
this.testSubscriptions.set(subscriptionKey, { store: disposable, collection, subscribeFn });
}
/**
@@ -162,217 +219,193 @@ export class ExtHostTesting implements ExtHostTestingShape {
* providers to be run.
* @override
*/
public async $runTestsForProvider(req: RunTestForProviderRequest): Promise<RunTestsResult> {
public async $runTestsForProvider(req: RunTestForProviderRequest, cancellation: CancellationToken): Promise<RunTestsResult> {
const provider = this.providers.get(req.providerId);
if (!provider || !provider.runTests) {
return EMPTY_TEST_RESULT;
}
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual).filter(isDefined);
const tests = req.ids.map(id => this.ownedTests.getTestById(id)?.actual)
.filter(isDefined)
// Only send the actual TestItem's to the user to run.
.map(t => t instanceof TestItemFilteredWrapper ? t.actual : t);
if (!tests.length) {
return EMPTY_TEST_RESULT;
}
await provider.runTests({ tests, debug: req.debug }, CancellationToken.None);
return EMPTY_TEST_RESULT;
}
}
const keyMap: { [K in keyof Omit<RequiredTestItem, 'children'>]: null } = {
label: null,
location: null,
state: null,
debuggable: null,
description: null,
runnable: null
};
const simpleProps = Object.keys(keyMap) as ReadonlyArray<keyof typeof keyMap>;
const itemEqualityComparator = (a: vscode.TestItem) => {
const values: unknown[] = [];
for (const prop of simpleProps) {
values.push(a[prop]);
}
return (b: vscode.TestItem) => {
for (let i = 0; i < simpleProps.length; i++) {
if (values[i] !== b[simpleProps[i]]) {
return false;
try {
await provider.runTests({ tests, debug: req.debug }, cancellation);
for (const { collection } of this.testSubscriptions.values()) {
collection.flushDiff(); // ensure all states are updated
}
return EMPTY_TEST_RESULT;
} catch (e) {
console.error(e); // so it appears to attached debuggers
throw e;
}
}
public $lookupTest(req: TestIdWithProvider): Promise<InternalTestItem | undefined> {
const owned = this.ownedTests.getTestById(req.testId);
if (!owned) {
return Promise.resolve(undefined);
}
return true;
};
};
/**
* @private
*/
export interface OwnedCollectionTestItem extends InternalTestItem {
actual: vscode.TestItem;
previousChildren: Set<string>;
previousEquals: (v: vscode.TestItem) => boolean;
}
/**
* @private
*/
export class OwnedTestCollection {
protected readonly testIdToInternal = new Map<string, OwnedCollectionTestItem>();
/**
* Gets test information by ID, if it was defined and still exists in this
* extension host.
*/
public getTestById(id: string) {
return this.testIdToInternal.get(id);
const { actual, previousChildren, previousEquals, ...item } = owned;
return Promise.resolve(item);
}
/**
* Creates a new test collection for a specific hierarchy for a workspace
* or document observation.
*/
public createForHierarchy(publishDiff: (diff: TestsDiff) => void = () => undefined) {
return new SingleUseTestCollection(this.testIdToInternal, publishDiff);
}
}
/**
* Maintains tests created and registered for a single set of hierarchies
* for a workspace or document.
* @private
*/
export class SingleUseTestCollection implements IDisposable {
protected readonly testItemToInternal = new Map<vscode.TestItem, OwnedCollectionTestItem>();
protected diff: TestsDiff = [];
private disposed = false;
constructor(private readonly testIdToInternal: Map<string, OwnedCollectionTestItem>, private readonly publishDiff: (diff: TestsDiff) => void) { }
/**
* Adds a new root node to the collection.
*/
public addRoot(item: vscode.TestItem, providerId: string) {
this.addItem(item, providerId, null);
this.throttleSendDiff();
}
/**
* Gets test information by its reference, if it was defined and still exists
* in this extension host.
*/
public getTestByReference(item: vscode.TestItem) {
return this.testItemToInternal.get(item);
}
/**
* Should be called when an item change is fired on the test provider.
*/
public onItemChange(item: vscode.TestItem, providerId: string) {
const existing = this.testItemToInternal.get(item);
if (!existing) {
if (!this.disposed) {
console.warn(`Received a TestProvider.onDidChangeTest for a test that wasn't seen before as a child.`);
}
private createDefaultDocumentTestHierarchy(provider: vscode.TestProvider, document: vscode.TextDocument, folder: vscode.WorkspaceFolder | undefined): vscode.TestHierarchy<vscode.TestItem> | undefined {
if (!folder) {
return;
}
this.addItem(item, providerId, existing.parent);
this.throttleSendDiff();
}
/**
* Gets a diff of all changes that have been made, and clears the diff queue.
*/
public collectDiff() {
const diff = this.diff;
this.diff = [];
return diff;
}
public dispose() {
for (const item of this.testItemToInternal.values()) {
this.testIdToInternal.delete(item.id);
const workspaceHierarchy = provider.createWorkspaceTestHierarchy?.(folder);
if (!workspaceHierarchy) {
return;
}
this.testIdToInternal.clear();
this.diff = [];
this.disposed = true;
}
const onDidChangeTest = new Emitter<vscode.TestItem>();
workspaceHierarchy.onDidChangeTest(node => {
const wrapper = TestItemFilteredWrapper.getWrapperForTestItem(node, document);
const previouslySeen = wrapper.hasNodeMatchingFilter;
protected getId(): string {
return generateUuid();
}
if (previouslySeen) {
// reset cache and get whether you can currently see the TestItem.
wrapper.reset();
const currentlySeen = wrapper.hasNodeMatchingFilter;
private addItem(actual: vscode.TestItem, providerId: string, parent: string | null) {
let internal = this.testItemToInternal.get(actual);
if (!internal) {
internal = {
actual,
id: this.getId(),
parent,
item: TestItem.from(actual),
providerId,
previousChildren: new Set(),
previousEquals: itemEqualityComparator(actual),
};
if (currentlySeen) {
onDidChangeTest.fire(wrapper);
return;
}
this.testItemToInternal.set(actual, internal);
this.testIdToInternal.set(internal.id, internal);
this.diff.push([TestDiffOpType.Add, { id: internal.id, parent, providerId, item: internal.item }]);
} else if (!internal.previousEquals(actual)) {
internal.item = TestItem.from(actual);
internal.previousEquals = itemEqualityComparator(actual);
this.diff.push([TestDiffOpType.Update, { id: internal.id, parent, providerId, item: internal.item }]);
}
// If there are children, track which ones are deleted
// and recursively and/update them.
if (actual.children) {
const deletedChildren = internal.previousChildren;
const currentChildren = new Set<string>();
for (const child of actual.children) {
const c = this.addItem(child, providerId, internal.id);
deletedChildren.delete(c.id);
currentChildren.add(c.id);
// Fire the event to say that the current visible parent has changed.
onDidChangeTest.fire(wrapper.visibleParent);
return;
}
for (const child of deletedChildren) {
this.removeItembyId(child);
const previousParent = wrapper.visibleParent;
wrapper.reset();
const currentlySeen = wrapper.hasNodeMatchingFilter;
// It wasn't previously seen and isn't currently seen so
// nothing has actually changed.
if (!currentlySeen) {
return;
}
internal.previousChildren = currentChildren;
}
// The test is now visible so we need to refresh the cache
// of the previous visible parent and fire that it has changed.
previousParent.reset();
onDidChangeTest.fire(previousParent);
});
return {
root: TestItemFilteredWrapper.getWrapperForTestItem(workspaceHierarchy.root, document),
dispose: () => {
onDidChangeTest.dispose();
TestItemFilteredWrapper.removeFilter(document);
},
onDidChangeTest: onDidChangeTest.event
};
}
}
return internal;
/*
* A class which wraps a vscode.TestItem that provides the ability to filter a TestItem's children
* to only the children that are located in a certain vscode.Uri.
*/
export class TestItemFilteredWrapper implements vscode.TestItem {
private static wrapperMap = new WeakMap<vscode.TextDocument, WeakMap<vscode.TestItem, TestItemFilteredWrapper>>();
public static removeFilter(document: vscode.TextDocument): void {
this.wrapperMap.delete(document);
}
private removeItembyId(id: string) {
this.diff.push([TestDiffOpType.Remove, id]);
const queue = [this.testIdToInternal.get(id)];
while (queue.length) {
const item = queue.pop();
if (!item) {
continue;
}
this.testIdToInternal.delete(item.id);
this.testItemToInternal.delete(item.actual);
for (const child of item.previousChildren) {
queue.push(this.testIdToInternal.get(child));
}
// Wraps the TestItem specified in a TestItemFilteredWrapper and pulls from a cache if it already exists.
public static getWrapperForTestItem(item: vscode.TestItem, filterDocument: vscode.TextDocument, parent?: TestItemFilteredWrapper): TestItemFilteredWrapper {
let innerMap = this.wrapperMap.get(filterDocument);
if (innerMap?.has(item)) {
return innerMap.get(item)!;
}
if (!innerMap) {
innerMap = new WeakMap<vscode.TestItem, TestItemFilteredWrapper>();
this.wrapperMap.set(filterDocument, innerMap);
}
const w = new TestItemFilteredWrapper(item, filterDocument, parent);
innerMap.set(item, w);
return w;
}
@throttle(200)
protected throttleSendDiff() {
const diff = this.collectDiff();
if (diff.length) {
this.publishDiff(diff);
public get label() {
return this.actual.label;
}
public get debuggable() {
return this.actual.debuggable;
}
public get description() {
return this.actual.description;
}
public get location() {
return this.actual.location;
}
public get runnable() {
return this.actual.runnable;
}
public get state() {
return this.actual.state;
}
public get children() {
// We only want children that match the filter.
return this.getWrappedChildren().filter(child => child.hasNodeMatchingFilter);
}
public get visibleParent(): TestItemFilteredWrapper {
return this.hasNodeMatchingFilter ? this : this.parent!.visibleParent;
}
private matchesFilter: boolean | undefined;
// Determines if the TestItem matches the filter. This would be true if:
// 1. We don't have a parent (because the root is the workspace root node)
// 2. The URI of the current node matches the filter URI
// 3. Some child of the current node matches the filter URI
public get hasNodeMatchingFilter(): boolean {
if (this.matchesFilter === undefined) {
this.matchesFilter = !this.parent
|| this.actual.location?.uri.toString() === this.filterDocument.uri.toString()
|| this.getWrappedChildren().some(child => child.hasNodeMatchingFilter);
}
return this.matchesFilter;
}
// Reset the cache of whether or not you can see a node from a particular node
// up to it's visible parent.
public reset(): void {
if (this !== this.visibleParent) {
this.parent?.reset();
}
this.matchesFilter = undefined;
}
private constructor(public readonly actual: vscode.TestItem, private filterDocument: vscode.TextDocument, private readonly parent?: TestItemFilteredWrapper) {
this.getWrappedChildren();
}
private getWrappedChildren() {
return this.actual.children?.map(t => TestItemFilteredWrapper.getWrapperForTestItem(t, this.filterDocument, this)) || [];
}
}
@@ -382,7 +415,7 @@ export class SingleUseTestCollection implements IDisposable {
interface MirroredCollectionTestItem extends IncrementalTestCollectionItem {
revived: vscode.TestItem;
depth: number;
wrapped?: vscode.TestItem;
wrapped?: vscode.RequiredTestItem;
}
class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollectionTestItem> {
@@ -411,7 +444,7 @@ class MirroredChangeCollector extends IncrementalChangeCollector<MirroredCollect
* @override
*/
public update(node: MirroredCollectionTestItem): void {
Object.assign(node.revived, TestItem.to(node.item));
Object.assign(node.revived, TestItem.toShallow(node.item));
if (!this.added.has(node)) {
this.updated.add(node);
}
@@ -545,8 +578,8 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection<Mi
/**
* Translates the item IDs to TestItems for exposure to extensions.
*/
public getAllAsTestItem(itemIds: Iterable<string>): vscode.TestItem[] {
let output: vscode.TestItem[] = [];
public getAllAsTestItem(itemIds: Iterable<string>): vscode.RequiredTestItem[] {
let output: vscode.RequiredTestItem[] = [];
for (const itemId of itemIds) {
const item = this.items.get(itemId);
if (item) {
@@ -577,7 +610,7 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection<Mi
* @override
*/
protected createItem(item: InternalTestItem, parent?: MirroredCollectionTestItem): MirroredCollectionTestItem {
return { ...item, revived: TestItem.to(item.item), depth: parent ? parent.depth + 1 : 0, children: new Set() };
return { ...item, revived: TestItem.toShallow(item.item), depth: parent ? parent.depth + 1 : 0, children: new Set() };
}
/**
@@ -590,9 +623,9 @@ export class MirroredTestCollection extends AbstractIncrementalTestCollection<Mi
/**
* Gets the public test item instance for the given mirrored record.
*/
public getPublicTestItem(item: MirroredCollectionTestItem): vscode.TestItem {
public getPublicTestItem(item: MirroredCollectionTestItem): vscode.RequiredTestItem {
if (!item.wrapped) {
item.wrapped = new ExtHostTestItem(item, this);
item.wrapped = new TestItemFromMirror(item, this);
}
return item.wrapped;
@@ -605,10 +638,11 @@ const getMirroredItemId = (item: vscode.TestItem) => {
const MirroredItemId = Symbol('MirroredItemId');
class ExtHostTestItem implements vscode.TestItem, RequiredTestItem {
class TestItemFromMirror implements vscode.RequiredTestItem {
readonly #internal: MirroredCollectionTestItem;
readonly #collection: MirroredTestCollection;
public get id() { return this.#internal.revived.id!; }
public get label() { return this.#internal.revived.label; }
public get description() { return this.#internal.revived.description; }
public get state() { return this.#internal.revived.state; }
@@ -627,14 +661,18 @@ class ExtHostTestItem implements vscode.TestItem, RequiredTestItem {
}
public toJSON() {
const serialized: RequiredTestItem = {
const serialized: vscode.RequiredTestItem & TestIdWithProvider = {
id: this.id,
label: this.label,
description: this.description,
state: this.state,
location: this.location,
runnable: this.runnable,
debuggable: this.debuggable,
children: this.children.map(c => (c as ExtHostTestItem).toJSON()),
children: this.children.map(c => (c as TestItemFromMirror).toJSON()),
providerId: this.#internal.providerId,
testId: this.#internal.id,
};
return serialized;
@@ -655,6 +693,7 @@ abstract class AbstractTestObserverFactory {
const resourceKey = resourceUri.toString();
const resource = this.resources.get(resourceKey) ?? this.createObserverData(resourceUri);
resource.pendingDeletion?.dispose();
resource.observers++;
return {
@@ -778,16 +817,9 @@ class TextDocumentTestObserverFactory extends AbstractTestObserverFactory {
const uriString = resourceUri.toString();
this.diffListeners.set(uriString, onDiff);
const disposeListener = this.documents.onDidRemoveDocuments(evt => {
if (evt.some(delta => delta.document.uri.toString() === uriString)) {
this.unlisten(resourceUri);
}
});
this.proxy.$subscribeToDiffs(ExtHostTestingResource.TextDocument, resourceUri);
return new Disposable(() => {
this.proxy.$unsubscribeFromDiffs(ExtHostTestingResource.TextDocument, resourceUri);
disposeListener.dispose();
this.diffListeners.delete(uriString);
});
}

View File

@@ -21,6 +21,7 @@ import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'
import { MarkdownString } from 'vs/workbench/api/common/extHostTypeConverters';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { Command } from 'vs/editor/common/modes';
type TreeItemHandle = string;
@@ -132,12 +133,12 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape {
return treeView.hasResolve;
}
$resolve(treeViewId: string, treeItemHandle: string): Promise<ITreeItem | undefined> {
$resolve(treeViewId: string, treeItemHandle: string, token: vscode.CancellationToken): Promise<ITreeItem | undefined> {
const treeView = this.treeViews.get(treeViewId);
if (!treeView) {
throw new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId));
}
return treeView.resolveTreeItem(treeItemHandle);
return treeView.resolveTreeItem(treeItemHandle, token);
}
$setExpanded(treeViewId: string, treeItemHandle: string, expanded: boolean): void {
@@ -184,6 +185,7 @@ interface TreeNode extends IDisposable {
extensionItem: vscode.TreeItem;
parent: TreeNode | Root;
children?: TreeNode[];
disposableStore: DisposableStore;
}
class ExtHostTreeView<T> extends Disposable {
@@ -370,7 +372,7 @@ class ExtHostTreeView<T> extends Disposable {
return !!this.dataProvider.resolveTreeItem;
}
async resolveTreeItem(treeItemHandle: string): Promise<ITreeItem | undefined> {
async resolveTreeItem(treeItemHandle: string, token: vscode.CancellationToken): Promise<ITreeItem | undefined> {
if (!this.dataProvider.resolveTreeItem) {
return;
}
@@ -378,9 +380,10 @@ class ExtHostTreeView<T> extends Disposable {
if (element) {
const node = this.nodes.get(element);
if (node) {
const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element) ?? node.extensionItem;
// Resolvable elements. Currently only tooltip.
const resolve = await this.dataProvider.resolveTreeItem(node.extensionItem, element, token) ?? node.extensionItem;
// Resolvable elements. Currently only tooltip and command.
node.item.tooltip = this.getTooltip(resolve.tooltip);
node.item.command = this.getCommand(node.disposableStore, resolve.command);
return node.item;
}
}
@@ -573,8 +576,12 @@ class ExtHostTreeView<T> extends Disposable {
return tooltip;
}
private getCommand(disposable: DisposableStore, command?: vscode.Command): Command | undefined {
return command ? this.commands.toInternal(command, disposable) : undefined;
}
private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode | Root): TreeNode {
const disposable = new DisposableStore();
const disposableStore = new DisposableStore();
const handle = this.createHandle(element, extensionTreeItem, parent);
const icon = this.getLightIconPath(extensionTreeItem);
const item: ITreeItem = {
@@ -584,7 +591,7 @@ class ExtHostTreeView<T> extends Disposable {
description: extensionTreeItem.description,
resourceUri: extensionTreeItem.resourceUri,
tooltip: this.getTooltip(extensionTreeItem.tooltip),
command: extensionTreeItem.command ? this.commands.toInternal(extensionTreeItem.command, disposable) : undefined,
command: this.getCommand(disposableStore, extensionTreeItem.command),
contextValue: extensionTreeItem.contextValue,
icon,
iconDark: this.getDarkIconPath(extensionTreeItem) || icon,
@@ -598,7 +605,8 @@ class ExtHostTreeView<T> extends Disposable {
extensionItem: extensionTreeItem,
parent,
children: undefined,
dispose(): void { disposable.dispose(); }
disposableStore,
dispose(): void { disposableStore.dispose(); }
};
}

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtHostTunnelServiceShape, MainContext, MainThreadTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostTunnelServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import * as vscode from 'vscode';
import { RemoteTunnel, TunnelCreationOptions, TunnelOptions } from 'vs/platform/remote/common/tunnel';
@@ -11,18 +11,27 @@ import { IDisposable } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
export interface TunnelDto {
remoteAddress: { port: number, host: string };
localAddress: { port: number, host: string } | string;
public: boolean;
}
export namespace TunnelDto {
export function fromApiTunnel(tunnel: vscode.Tunnel): TunnelDto {
return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress };
return { remoteAddress: tunnel.remoteAddress, localAddress: tunnel.localAddress, public: !!tunnel.public };
}
export function fromServiceTunnel(tunnel: RemoteTunnel): TunnelDto {
return { remoteAddress: { host: tunnel.tunnelRemoteHost, port: tunnel.tunnelRemotePort }, localAddress: tunnel.localAddress };
return {
remoteAddress: {
host: tunnel.tunnelRemoteHost,
port: tunnel.tunnelRemotePort
},
localAddress: tunnel.localAddress,
public: tunnel.public
};
}
}
@@ -44,12 +53,13 @@ export const IExtHostTunnelService = createDecorator<IExtHostTunnelService>('IEx
export class ExtHostTunnelService implements IExtHostTunnelService {
declare readonly _serviceBrand: undefined;
onDidChangeTunnels: vscode.Event<void> = (new Emitter<void>()).event;
private readonly _proxy: MainThreadTunnelServiceShape;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
) {
this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService);
}
async $applyCandidateFilter(candidates: CandidatePort[]): Promise<CandidatePort[]> {
return candidates;
}
async openTunnel(extension: IExtensionDescription, forward: TunnelOptions): Promise<vscode.Tunnel | undefined> {
@@ -59,10 +69,10 @@ export class ExtHostTunnelService implements IExtHostTunnelService {
return [];
}
async setTunnelExtensionFunctions(provider: vscode.RemoteAuthorityResolver | undefined): Promise<IDisposable> {
await this._proxy.$tunnelServiceReady();
return { dispose: () => { } };
}
$forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto> | undefined { return undefined; }
async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto | undefined> { return undefined; }
async $closeTunnel(remote: { host: string, port: number }): Promise<void> { }
async $onDidTunnelsChange(): Promise<void> { }
async $registerCandidateFinder(): Promise<void> { }
}

View File

@@ -1015,6 +1015,29 @@ export namespace SignatureHelp {
}
}
export namespace InlineHint {
export function from(hint: vscode.InlineHint): modes.InlineHint {
return {
text: hint.text,
range: Range.from(hint.range),
description: hint.description && MarkdownString.fromStrict(hint.description),
whitespaceBefore: hint.whitespaceBefore,
whitespaceAfter: hint.whitespaceAfter
};
}
export function to(hint: modes.InlineHint): vscode.InlineHint {
return new types.InlineHint(
hint.text,
Range.to(hint.range),
htmlContent.isMarkdownString(hint.description) ? MarkdownString.to(hint.description) : hint.description,
hint.whitespaceBefore,
hint.whitespaceAfter
);
}
}
export namespace DocumentLink {
export function from(link: vscode.DocumentLink): modes.ILink {
@@ -1389,19 +1412,21 @@ export namespace TestState {
export namespace TestItem {
export function from(item: vscode.TestItem): ITestItem {
export function from(item: vscode.TestItem, parentExtId?: string): ITestItem {
return {
extId: item.id ?? (parentExtId ? `${parentExtId}\0${item.label}` : item.label),
label: item.label,
location: item.location ? location.from(item.location) : undefined,
debuggable: item.debuggable,
debuggable: item.debuggable ?? false,
description: item.description,
runnable: item.runnable,
runnable: item.runnable ?? true,
state: TestState.from(item.state),
};
}
export function to(item: ITestItem): vscode.TestItem {
export function toShallow(item: ITestItem): Omit<vscode.RequiredTestItem, 'children'> {
return {
id: item.extId,
label: item.label,
location: item.location && location.to({
range: item.location.range,

View File

@@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri';
import { generateUuid } from 'vs/base/common/uuid';
import { FileSystemProviderErrorCode, markAsFileSystemProviderError } from 'vs/platform/files/common/files';
import { RemoteAuthorityResolverErrorCode } from 'vs/platform/remote/common/remoteAuthorityResolver';
import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { addIdToOutput, CellEditType, ICellEditOperation, IDisplayOutput, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import type * as vscode from 'vscode';
function es5ClassCompat(target: Function): any {
@@ -638,7 +638,7 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit {
// --- notebook
replaceNotebookMetadata(uri: URI, value: vscode.NotebookDocumentMetadata, metadata?: vscode.WorkspaceEditEntryMetadata): void {
this._edits.push({ _type: FileEditType.Cell, metadata, uri, notebookMetadata: value });
this._edits.push({ _type: FileEditType.Cell, metadata, uri, edit: { editType: CellEditType.DocumentMetadata, metadata: { ...notebookDocumentMetadataDefaults, ...value } }, notebookMetadata: value });
}
replaceNotebookCells(uri: URI, start: number, end: number, cells: vscode.NotebookCellData[], metadata?: vscode.WorkspaceEditEntryMetadata): void {
@@ -1373,6 +1373,23 @@ export enum SignatureHelpTriggerKind {
ContentChange = 3,
}
@es5ClassCompat
export class InlineHint {
text: string;
range: Range;
description?: string | vscode.MarkdownString;
whitespaceBefore?: boolean;
whitespaceAfter?: boolean;
constructor(text: string, range: Range, description?: string | vscode.MarkdownString, whitespaceBefore?: boolean, whitespaceAfter?: boolean) {
this.text = text;
this.range = range;
this.description = description;
this.whitespaceBefore = whitespaceBefore;
this.whitespaceAfter = whitespaceAfter;
}
}
export enum CompletionTriggerKind {
Invoke = 0,
TriggerCharacter = 1,
@@ -2298,9 +2315,6 @@ export class FunctionBreakpoint extends Breakpoint {
constructor(functionName: string, enabled?: boolean, condition?: string, hitCondition?: string, logMessage?: string) {
super(enabled, condition, hitCondition, logMessage);
if (!functionName) {
throw illegalArgument('functionName');
}
this.functionName = functionName;
}
}
@@ -2858,7 +2872,8 @@ export enum NotebookCellStatusBarAlignment {
export enum NotebookEditorRevealType {
Default = 0,
InCenter = 1,
InCenterIfOutsideViewport = 2
InCenterIfOutsideViewport = 2,
AtTop = 3
}
@@ -2924,11 +2939,12 @@ export class LinkedEditingRanges {
//#region Testing
export enum TestRunState {
Unset = 0,
Running = 1,
Passed = 2,
Failed = 3,
Skipped = 4,
Errored = 5
Queued = 1,
Running = 2,
Passed = 3,
Failed = 4,
Skipped = 5,
Errored = 6
}
export enum TestMessageSeverity {
@@ -2963,15 +2979,15 @@ export class TestState {
}
}
type AllowedUndefined = 'description' | 'location';
/**
* Test item without any optional properties. Only some properties are
* permitted to be undefined, but they must still exist.
*/
export type RequiredTestItem = {
[K in keyof Required<vscode.TestItem>]: K extends AllowedUndefined ? vscode.TestItem[K] : Required<vscode.TestItem>[K]
};
export type RequiredTestItem = vscode.RequiredTestItem;
export type TestItem = vscode.TestItem;
//#endregion
export enum ExternalUriOpenerPriority {
None = 0,
Option = 1,
Default = 2,
Preferred = 3,
}

View File

@@ -0,0 +1,74 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from 'vs/base/common/cancellation';
import { toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { URI, UriComponents } from 'vs/base/common/uri';
import * as modes from 'vs/editor/common/modes';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import type * as vscode from 'vscode';
import { ExtHostUriOpenersShape, IMainContext, MainContext, MainThreadUriOpenersShape } from './extHost.protocol';
export class ExtHostUriOpeners implements ExtHostUriOpenersShape {
private static readonly supportedSchemes = new Set<string>([Schemas.http, Schemas.https]);
private readonly _proxy: MainThreadUriOpenersShape;
private readonly _openers = new Map<string, vscode.ExternalUriOpener>();
constructor(
mainContext: IMainContext,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadUriOpeners);
}
registerExternalUriOpener(
extensionId: ExtensionIdentifier,
id: string,
opener: vscode.ExternalUriOpener,
metadata: vscode.ExternalUriOpenerMetadata,
): vscode.Disposable {
if (this._openers.has(id)) {
throw new Error(`Opener with id '${id}' already registered`);
}
const invalidScheme = metadata.schemes.find(scheme => !ExtHostUriOpeners.supportedSchemes.has(scheme));
if (invalidScheme) {
throw new Error(`Scheme '${invalidScheme}' is not supported. Only http and https are currently supported.`);
}
this._openers.set(id, opener);
this._proxy.$registerUriOpener(id, metadata.schemes, extensionId, metadata.label);
return toDisposable(() => {
this._openers.delete(id);
this._proxy.$unregisterUriOpener(id);
});
}
async $canOpenUri(id: string, uriComponents: UriComponents, token: CancellationToken): Promise<modes.ExternalUriOpenerPriority> {
const opener = this._openers.get(id);
if (!opener) {
throw new Error(`Unknown opener with id: ${id}`);
}
const uri = URI.revive(uriComponents);
return opener.canOpenExternalUri(uri, token);
}
async $openUri(id: string, context: { resolvedUri: UriComponents, sourceUri: UriComponents }, token: CancellationToken): Promise<void> {
const opener = this._openers.get(id);
if (!opener) {
throw new Error(`Unknown opener id: '${id}'`);
}
return opener.openExternalUri(URI.revive(context.resolvedUri), {
sourceUri: URI.revive(context.sourceUri)
}, token);
}
}

View File

@@ -287,8 +287,8 @@ export class ExtHostWebviewPanels implements extHostProtocol.ExtHostWebviewPanel
await serializer.deserializeWebviewPanel(revivedPanel, state);
}
public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: number, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) {
const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, typeof position === 'number' && position >= 0 ? typeConverters.ViewColumn.to(position) : undefined, options, webview);
public createNewWebviewPanel(webviewHandle: string, viewType: string, title: string, position: vscode.ViewColumn, options: modes.IWebviewOptions & modes.IWebviewPanelOptions, webview: ExtHostWebview) {
const panel = new ExtHostWebviewPanel(webviewHandle, this._proxy, viewType, title, position, options, webview);
this._webviewPanels.set(webviewHandle, panel);
return panel;
}

View File

@@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event';
import { TernarySearchTree } from 'vs/base/common/map';
import { Schemas } from 'vs/base/common/network';
import { Counter } from 'vs/base/common/numbers';
import { basename, basenameOrAuthority, dirname, isEqual, relativePath } from 'vs/base/common/resources';
import { basename, basenameOrAuthority, dirname, ExtUri, relativePath } from 'vs/base/common/resources';
import { compare } from 'vs/base/common/strings';
import { withUndefinedAsNull } from 'vs/base/common/types';
import { URI } from 'vs/base/common/uri';
@@ -37,27 +37,27 @@ export interface IExtHostWorkspaceProvider {
resolveProxy(url: string): Promise<string | undefined>;
}
function isFolderEqual(folderA: URI, folderB: URI): boolean {
return isEqual(folderA, folderB);
function isFolderEqual(folderA: URI, folderB: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean {
return new ExtUri(uri => ignorePathCasing(uri, extHostFileSystemInfo)).isEqual(folderA, folderB);
}
function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number {
return isFolderEqual(a.uri, b.uri) ? 0 : compare(a.uri.toString(), b.uri.toString());
function compareWorkspaceFolderByUri(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number {
return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? 0 : compare(a.uri.toString(), b.uri.toString());
}
function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder): number {
function compareWorkspaceFolderByUriAndNameAndIndex(a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo): number {
if (a.index !== b.index) {
return a.index < b.index ? -1 : 1;
}
return isFolderEqual(a.uri, b.uri) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString());
return isFolderEqual(a.uri, b.uri, extHostFileSystemInfo) ? compare(a.name, b.name) : compare(a.uri.toString(), b.uri.toString());
}
function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder) => number): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } {
const oldSortedFolders = oldFolders.slice(0).sort(compare);
const newSortedFolders = newFolders.slice(0).sort(compare);
function delta(oldFolders: vscode.WorkspaceFolder[], newFolders: vscode.WorkspaceFolder[], compare: (a: vscode.WorkspaceFolder, b: vscode.WorkspaceFolder, extHostFileSystemInfo: IExtHostFileSystemInfo) => number, extHostFileSystemInfo: IExtHostFileSystemInfo): { removed: vscode.WorkspaceFolder[], added: vscode.WorkspaceFolder[] } {
const oldSortedFolders = oldFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo));
const newSortedFolders = newFolders.slice(0).sort((a, b) => compare(a, b, extHostFileSystemInfo));
return arrayDelta(oldSortedFolders, newSortedFolders, compare);
return arrayDelta(oldSortedFolders, newSortedFolders, (a, b) => compare(a, b, extHostFileSystemInfo));
}
function ignorePathCasing(uri: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): boolean {
@@ -87,7 +87,7 @@ class ExtHostWorkspaceImpl extends Workspace {
if (previousConfirmedWorkspace) {
folders.forEach((folderData, index) => {
const folderUri = URI.revive(folderData.uri);
const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri);
const existingFolder = ExtHostWorkspaceImpl._findFolder(previousUnconfirmedWorkspace || previousConfirmedWorkspace, folderUri, extHostFileSystemInfo);
if (existingFolder) {
existingFolder.name = folderData.name;
@@ -106,15 +106,15 @@ class ExtHostWorkspaceImpl extends Workspace {
newWorkspaceFolders.sort((f1, f2) => f1.index < f2.index ? -1 : 1);
const workspace = new ExtHostWorkspaceImpl(id, name, newWorkspaceFolders, configuration ? URI.revive(configuration) : null, !!isUntitled, uri => ignorePathCasing(uri, extHostFileSystemInfo));
const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri);
const { added, removed } = delta(oldWorkspace ? oldWorkspace.workspaceFolders : [], workspace.workspaceFolders, compareWorkspaceFolderByUri, extHostFileSystemInfo);
return { workspace, added, removed };
}
private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI): MutableWorkspaceFolder | undefined {
private static _findFolder(workspace: ExtHostWorkspaceImpl, folderUriToFind: URI, extHostFileSystemInfo: IExtHostFileSystemInfo): MutableWorkspaceFolder | undefined {
for (let i = 0; i < workspace.folders.length; i++) {
const folder = workspace.workspaceFolders[i];
if (isFolderEqual(folder.uri, folderUriToFind)) {
if (isFolderEqual(folder.uri, folderUriToFind, extHostFileSystemInfo)) {
return folder;
}
}
@@ -254,7 +254,7 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
const validatedDistinctWorkspaceFoldersToAdd: { uri: vscode.Uri, name?: string }[] = [];
if (Array.isArray(workspaceFoldersToAdd)) {
workspaceFoldersToAdd.forEach(folderToAdd => {
if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri))) {
if (URI.isUri(folderToAdd.uri) && !validatedDistinctWorkspaceFoldersToAdd.some(f => isFolderEqual(f.uri, folderToAdd.uri, this._extHostFileSystemInfo))) {
validatedDistinctWorkspaceFoldersToAdd.push({ uri: folderToAdd.uri, name: folderToAdd.name || basenameOrAuthority(folderToAdd.uri) });
}
});
@@ -283,13 +283,13 @@ export class ExtHostWorkspace implements ExtHostWorkspaceShape, IExtHostWorkspac
for (let i = 0; i < newWorkspaceFolders.length; i++) {
const folder = newWorkspaceFolders[i];
if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri))) {
if (newWorkspaceFolders.some((otherFolder, index) => index !== i && isFolderEqual(folder.uri, otherFolder.uri, this._extHostFileSystemInfo))) {
return false; // cannot add the same folder multiple times
}
}
newWorkspaceFolders.forEach((f, index) => f.index = index); // fix index
const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex);
const { added, removed } = delta(currentWorkspaceFolders, newWorkspaceFolders, compareWorkspaceFolderByUriAndNameAndIndex, this._extHostFileSystemInfo);
if (added.length === 0 && removed.length === 0) {
return false; // nothing actually changed
}

View File

@@ -1,152 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { VSBuffer } from 'vs/base/common/buffer';
import * as platform from 'vs/base/common/platform';
export interface IFullSemanticTokensDto {
id: number;
type: 'full';
data: Uint32Array;
}
export interface IDeltaSemanticTokensDto {
id: number;
type: 'delta';
deltas: { start: number; deleteCount: number; data?: Uint32Array; }[];
}
export type ISemanticTokensDto = IFullSemanticTokensDto | IDeltaSemanticTokensDto;
const enum EncodedSemanticTokensType {
Full = 1,
Delta = 2
}
function reverseEndianness(arr: Uint8Array): void {
for (let i = 0, len = arr.length; i < len; i += 4) {
// flip bytes 0<->3 and 1<->2
const b0 = arr[i + 0];
const b1 = arr[i + 1];
const b2 = arr[i + 2];
const b3 = arr[i + 3];
arr[i + 0] = b3;
arr[i + 1] = b2;
arr[i + 2] = b1;
arr[i + 3] = b0;
}
}
function toLittleEndianBuffer(arr: Uint32Array): VSBuffer {
const uint8Arr = new Uint8Array(arr.buffer, arr.byteOffset, arr.length * 4);
if (!platform.isLittleEndian()) {
// the byte order must be changed
reverseEndianness(uint8Arr);
}
return VSBuffer.wrap(uint8Arr);
}
function fromLittleEndianBuffer(buff: VSBuffer): Uint32Array {
const uint8Arr = buff.buffer;
if (!platform.isLittleEndian()) {
// the byte order must be changed
reverseEndianness(uint8Arr);
}
if (uint8Arr.byteOffset % 4 === 0) {
return new Uint32Array(uint8Arr.buffer, uint8Arr.byteOffset, uint8Arr.length / 4);
} else {
// unaligned memory access doesn't work on all platforms
const data = new Uint8Array(uint8Arr.byteLength);
data.set(uint8Arr);
return new Uint32Array(data.buffer, data.byteOffset, data.length / 4);
}
}
export function encodeSemanticTokensDto(semanticTokens: ISemanticTokensDto): VSBuffer {
const dest = new Uint32Array(encodeSemanticTokensDtoSize(semanticTokens));
let offset = 0;
dest[offset++] = semanticTokens.id;
if (semanticTokens.type === 'full') {
dest[offset++] = EncodedSemanticTokensType.Full;
dest[offset++] = semanticTokens.data.length;
dest.set(semanticTokens.data, offset); offset += semanticTokens.data.length;
} else {
dest[offset++] = EncodedSemanticTokensType.Delta;
dest[offset++] = semanticTokens.deltas.length;
for (const delta of semanticTokens.deltas) {
dest[offset++] = delta.start;
dest[offset++] = delta.deleteCount;
if (delta.data) {
dest[offset++] = delta.data.length;
dest.set(delta.data, offset); offset += delta.data.length;
} else {
dest[offset++] = 0;
}
}
}
return toLittleEndianBuffer(dest);
}
function encodeSemanticTokensDtoSize(semanticTokens: ISemanticTokensDto): number {
let result = 0;
result += (
+ 1 // id
+ 1 // type
);
if (semanticTokens.type === 'full') {
result += (
+ 1 // data length
+ semanticTokens.data.length
);
} else {
result += (
+ 1 // delta count
);
result += (
+ 1 // start
+ 1 // deleteCount
+ 1 // data length
) * semanticTokens.deltas.length;
for (const delta of semanticTokens.deltas) {
if (delta.data) {
result += delta.data.length;
}
}
}
return result;
}
export function decodeSemanticTokensDto(_buff: VSBuffer): ISemanticTokensDto {
const src = fromLittleEndianBuffer(_buff);
let offset = 0;
const id = src[offset++];
const type: EncodedSemanticTokensType = src[offset++];
if (type === EncodedSemanticTokensType.Full) {
const length = src[offset++];
const data = src.subarray(offset, offset + length); offset += length;
return {
id: id,
type: 'full',
data: data
};
}
const deltaCount = src[offset++];
let deltas: { start: number; deleteCount: number; data?: Uint32Array; }[] = [];
for (let i = 0; i < deltaCount; i++) {
const start = src[offset++];
const deleteCount = src[offset++];
const length = src[offset++];
let data: Uint32Array | undefined;
if (length > 0) {
data = src.subarray(offset, offset + length); offset += length;
}
deltas[i] = { start, deleteCount, data };
}
return {
id: id,
type: 'delta',
deltas: deltas
};
}

View File

@@ -119,8 +119,7 @@ export function checkGlobFileExists(
const queryBuilder = instantiationService.createInstance(QueryBuilder);
const query = queryBuilder.file(folders.map(folder => toWorkspaceFolder(URI.revive(folder))), {
_reason: 'checkExists',
includePattern: includes.join(', '),
expandPatterns: true,
includePattern: includes,
exists: true
});

View File

@@ -41,7 +41,15 @@ export interface RunCommandPipeArgs {
args: any[];
}
export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs;
export interface ExtensionManagementPipeArgs {
type: 'extensionManagement';
list?: { showVersions?: boolean, category?: string; };
install?: string[];
uninstall?: string[];
force?: boolean;
}
export type PipeCommand = OpenCommandPipeArgs | StatusPipeArgs | RunCommandPipeArgs | OpenExternalCommandPipeArgs | ExtensionManagementPipeArgs;
export interface ICommandsExecuter {
executeCommand<T>(id: string, ...args: any[]): Promise<T>;
@@ -102,6 +110,10 @@ export class CLIServerBase {
this.runCommand(data, res)
.catch(this.logService.error);
break;
case 'extensionManagement':
this.manageExtensions(data, res)
.catch(this.logService.error);
break;
default:
res.writeHead(404);
res.write(`Unknown message type: ${data.type}`, err => {
@@ -150,14 +162,34 @@ export class CLIServerBase {
res.end();
}
private openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) {
private async openExternal(data: OpenExternalCommandPipeArgs, res: http.ServerResponse) {
for (const uri of data.uris) {
this._commands.executeCommand('_workbench.openExternal', URI.parse(uri), { allowTunneling: true });
await this._commands.executeCommand('_remoteCLI.openExternal', URI.parse(uri), { allowTunneling: true });
}
res.writeHead(200);
res.end();
}
private async manageExtensions(data: ExtensionManagementPipeArgs, res: http.ServerResponse) {
console.log('server: manageExtensions');
try {
const toExtOrVSIX = (inputs: string[] | undefined) => inputs?.map(input => /\.vsix$/i.test(input) ? URI.parse(input) : input);
const commandArgs = {
list: data.list,
install: toExtOrVSIX(data.install),
uninstall: toExtOrVSIX(data.uninstall),
force: data.force
};
const output = await this._commands.executeCommand('_remoteCLI.manageExtensions', commandArgs, { allowTunneling: true });
res.writeHead(200);
res.write(output);
} catch (e) {
res.writeHead(500);
res.write(String(e));
}
res.end();
}
private async getStatus(data: StatusPipeArgs, res: http.ServerResponse) {
try {
const status = await this._commands.executeCommand('_issues.getSystemStatus');

View File

@@ -104,7 +104,7 @@ export class ExtHostDebugService extends ExtHostDebugServiceBase {
cwdForPrepareCommand = args.cwd;
}
terminal.show();
terminal.show(true);
const shellProcessId = await terminal.processId;

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as performance from 'vs/base/common/performance';
import { createApiFactoryAndRegisterActors } from 'vs/workbench/api/common/extHost.api.impl';
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
import { MainContext } from 'vs/workbench/api/common/extHost.protocol';
@@ -13,7 +14,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
import { URI } from 'vs/base/common/uri';
import { Schemas } from 'vs/base/common/network';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
class NodeModuleRequireInterceptor extends RequireInterceptor {
@@ -62,10 +63,12 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
// Module loading tricks
const interceptor = this._instaService.createInstance(NodeModuleRequireInterceptor, extensionApiFactory, this._registry);
await interceptor.install();
performance.mark('code/extHost/didInitAPI');
// Do this when extension service exists, but extensions are not being activated yet.
const configProvider = await this._extHostConfiguration.getConfigProvider();
await connectProxyResolver(this._extHostWorkspace, configProvider, this, this._logService, this._mainThreadTelemetryProxy, this._initData);
performance.mark('code/extHost/didInitProxyResolver');
// Use IPC messages to forward console-calls, note that the console is
// already patched to use`process.send()`
@@ -84,7 +87,7 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
return extensionDescription.main;
}
protected _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
protected _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
if (module.scheme !== Schemas.file) {
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
}
@@ -93,10 +96,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
this._logService.info(`ExtensionService#loadCommonJSModule ${module.toString(true)}`);
this._logService.flush();
try {
if (extensionId) {
performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`);
}
r = require.__$__nodeRequire<T>(module.fsPath);
} catch (e) {
return Promise.reject(e);
} finally {
if (extensionId) {
performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`);
}
activationTimesBuilder.codeLoadingStop();
}
return Promise.resolve(r);

View File

@@ -50,11 +50,12 @@ export class ExtHostTask extends ExtHostTaskBase {
}
public async executeTask(extension: IExtensionDescription, task: vscode.Task): Promise<vscode.TaskExecution> {
if (!task.execution) {
const tTask = (task as types.Task);
if (!task.execution && (tTask._id === undefined)) {
throw new Error('Tasks to execute must include an execution');
}
const tTask = (task as types.Task);
// We have a preserved ID. So the task didn't change.
if (tTask._id !== undefined) {
// Always get the task execution first to prevent timing issues when retrieving it later
@@ -121,7 +122,7 @@ export class ExtHostTask extends ExtHostTaskBase {
private async getVariableResolver(workspaceFolders: vscode.WorkspaceFolder[]): Promise<ExtHostVariableResolverService> {
if (this._variableResolver === undefined) {
const configProvider = await this._configurationService.getConfigProvider();
this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment);
this._variableResolver = new ExtHostVariableResolverService(workspaceFolders, this._editorService, configProvider, process.env as IProcessEnvironment, this.workspaceService);
}
return this._variableResolver;
}

View File

@@ -17,13 +17,15 @@ import { ExtHostWorkspace, IExtHostWorkspace } from 'vs/workbench/api/common/ext
import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ExtHostVariableResolverService } from 'vs/workbench/api/common/extHostDebugService';
import { ExtHostDocumentsAndEditors, IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { getSystemShell, detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal';
import { detectAvailableShells } from 'vs/workbench/contrib/terminal/node/terminal';
import { getMainProcessParentEnv } from 'vs/workbench/contrib/terminal/node/terminalEnvironment';
import { BaseExtHostTerminalService, ExtHostTerminal } from 'vs/workbench/api/common/extHostTerminalService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { MergedEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableCollection';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { withNullAsUndefined } from 'vs/base/common/types';
import { getSystemShell, getSystemShellSync } from 'vs/base/node/shell';
import { generateUuid } from 'vs/base/common/uuid';
export class ExtHostTerminalService extends BaseExtHostTerminalService {
@@ -32,6 +34,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
// TODO: Pull this from main side
private _isWorkspaceShellAllowed: boolean = false;
private _defaultShell: string | undefined;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@@ -42,20 +45,26 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
@IExtHostInitDataService private _extHostInitDataService: IExtHostInitDataService
) {
super(true, extHostRpc);
// Getting the SystemShell is an async operation, however, the ExtHost terminal service is mostly synchronous
// and the API `vscode.env.shell` is also synchronous. The default shell _should_ be set when extensions are
// starting up but if not, we run getSystemShellSync below which gets a sane default.
getSystemShell(platform.platform).then(s => this._defaultShell = s);
this._updateLastActiveWorkspace();
this._updateVariableResolver();
this._registerListeners();
}
public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, { name, shellPath, shellArgs }, name);
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), { name, shellPath, shellArgs }, name);
this._terminals.push(terminal);
terminal.create(shellPath, shellArgs);
return terminal;
}
public createTerminalFromOptions(options: vscode.TerminalOptions, isFeatureTerminal?: boolean): vscode.Terminal {
const terminal = new ExtHostTerminal(this._proxy, options, options.name);
const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name);
this._terminals.push(terminal);
terminal.create(
withNullAsUndefined(options.shellPath),
@@ -76,10 +85,11 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
.inspect<string | string[]>(key.substr(key.lastIndexOf('.') + 1));
return this._apiInspectConfigToPlain<string | string[]>(setting);
};
return terminalEnvironment.getDefaultShell(
fetchSetting,
this._isWorkspaceShellAllowed,
getSystemShell(platform.platform),
this._defaultShell ?? getSystemShellSync(platform.platform),
process.env.hasOwnProperty('PROCESSOR_ARCHITEW6432'),
process.env.windir,
terminalEnvironment.createVariableResolver(this._lastActiveWorkspace, this._variableResolver),
@@ -139,7 +149,8 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
executable: shellLaunchConfigDto.executable,
args: shellLaunchConfigDto.args,
cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd),
env: shellLaunchConfigDto.env
env: shellLaunchConfigDto.env,
flowControl: shellLaunchConfigDto.flowControl
};
// Merge in shell and args from settings

View File

@@ -12,12 +12,16 @@ import { URI } from 'vs/base/common/uri';
import { exec } from 'child_process';
import * as resources from 'vs/base/common/resources';
import * as fs from 'fs';
import * as pfs from 'vs/base/node/pfs';
import { isLinux } from 'vs/base/common/platform';
import { IExtHostTunnelService, TunnelDto } from 'vs/workbench/api/common/extHostTunnelService';
import { asPromise } from 'vs/base/common/async';
import { Event, Emitter } from 'vs/base/common/event';
import { TunnelOptions, TunnelCreationOptions } from 'vs/platform/remote/common/tunnel';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { promisify } from 'util';
import { MovingAverage } from 'vs/base/common/numbers';
import { CandidatePort } from 'vs/workbench/services/remote/common/remoteExplorerService';
import { ILogService } from 'vs/platform/log/common/log';
class ExtensionTunnel implements vscode.Tunnel {
private _onDispose: Emitter<void> = new Emitter();
@@ -26,14 +30,106 @@ class ExtensionTunnel implements vscode.Tunnel {
constructor(
public readonly remoteAddress: { port: number, host: string },
public readonly localAddress: { port: number, host: string } | string,
private readonly _dispose: () => void) { }
private readonly _dispose: () => Promise<void>) { }
dispose(): void {
dispose(): Promise<void> {
this._onDispose.fire();
this._dispose();
return this._dispose();
}
}
export function getSockets(stdout: string): { pid: number, socket: number }[] {
const lines = stdout.trim().split('\n');
const mapped: { pid: number, socket: number }[] = [];
lines.forEach(line => {
const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!;
if (match && match.length >= 3) {
mapped.push({
pid: parseInt(match[1], 10),
socket: parseInt(match[2], 10)
});
}
});
return mapped;
}
export function loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] {
const table = ([] as Record<string, string>[]).concat(...stdouts.map(loadConnectionTable));
return [
...new Map(
table.filter(row => row.st === '0A')
.map(row => {
const address = row.local_address.split(':');
return {
socket: parseInt(row.inode, 10),
ip: parseIpAddress(address[0]),
port: parseInt(address[1], 16)
};
}).map(port => [port.ip + ':' + port.port, port])
).values()
];
}
export function parseIpAddress(hex: string): string {
let result = '';
if (hex.length === 8) {
for (let i = hex.length - 2; i >= 0; i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
}
}
} else {
for (let i = hex.length - 4; i >= 0; i -= 4) {
result += parseInt(hex.substr(i, 4), 16).toString(16);
if (i !== 0) {
result += ':';
}
}
}
return result;
}
export function loadConnectionTable(stdout: string): Record<string, string>[] {
const lines = stdout.trim().split('\n');
const names = lines.shift()!.trim().split(/\s+/)
.filter(name => name !== 'rx_queue' && name !== 'tm->when');
const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => {
obj[names[i] || i] = value;
return obj;
}, {} as Record<string, string>));
return table;
}
function knownExcludeCmdline(command: string): boolean {
return !!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/)
|| (command.indexOf('out/vs/server/main.js') !== -1)
|| (command.indexOf('_productName=VSCode') !== -1);
}
export async function findPorts(tcp: string, tcp6: string, procSockets: string, processes: { pid: number, cwd: string, cmd: string }[]): Promise<CandidatePort[]> {
const connections: { socket: number, ip: string, port: number }[] = loadListeningPorts(tcp, tcp6);
const sockets = getSockets(procSockets);
const socketMap = sockets.reduce((m, socket) => {
m[socket.socket] = socket;
return m;
}, {} as Record<string, typeof sockets[0]>);
const processMap = processes.reduce((m, process) => {
m[process.pid] = process;
return m;
}, {} as Record<string, typeof processes[0]>);
const ports: CandidatePort[] = [];
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
const command = processMap[socketMap[socket].pid].cmd;
if (!knownExcludeCmdline(command)) {
ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd, pid: socketMap[socket].pid });
}
});
return ports;
}
export class ExtHostTunnelService extends Disposable implements IExtHostTunnelService {
readonly _serviceBrand: undefined;
private readonly _proxy: MainThreadTunnelServiceShape;
@@ -42,15 +138,17 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
private _extensionTunnels: Map<string, Map<number, { tunnel: vscode.Tunnel, disposeListener: IDisposable }>> = new Map();
private _onDidChangeTunnels: Emitter<void> = new Emitter<void>();
onDidChangeTunnels: vscode.Event<void> = this._onDidChangeTunnels.event;
private _candidateFindingEnabled: boolean = false;
constructor(
@IExtHostRpcService extHostRpc: IExtHostRpcService,
@IExtHostInitDataService initData: IExtHostInitDataService
@IExtHostInitDataService initData: IExtHostInitDataService,
@ILogService private readonly logService: ILogService
) {
super();
this._proxy = extHostRpc.getProxy(MainContext.MainThreadTunnelService);
if (initData.remote.isRemote && initData.remote.authority) {
this.registerCandidateFinder();
if (isLinux && initData.remote.isRemote && initData.remote.authority) {
this._proxy.$setCandidateFinder();
}
}
@@ -70,18 +168,30 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
return this._proxy.$getTunnels();
}
registerCandidateFinder(): void {
// Every two seconds, scan to see if the candidate ports have changed;
if (isLinux) {
let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined;
setInterval(async () => {
const newPorts = await this.findCandidatePorts();
if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) {
oldPorts = newPorts;
this._proxy.$onFoundNewCandidates(oldPorts.filter(async (candidate) => await this._showCandidatePort(candidate.host, candidate.port, candidate.detail)));
return;
}
}, 2000);
private calculateDelay(movingAverage: number) {
// Some local testing indicated that the moving average might be between 50-100 ms.
return Math.max(movingAverage * 20, 2000);
}
async $registerCandidateFinder(enable: boolean): Promise<void> {
if (enable && this._candidateFindingEnabled) {
// already enabled
return;
}
this._candidateFindingEnabled = enable;
// Regularly scan to see if the candidate ports have changed.
let movingAverage = new MovingAverage();
let oldPorts: { host: string, port: number, detail: string }[] | undefined = undefined;
while (this._candidateFindingEnabled) {
const startTime = new Date().getTime();
const newPorts = await this.findCandidatePorts();
const timeTaken = new Date().getTime() - startTime;
movingAverage.update(timeTaken);
if (!oldPorts || (JSON.stringify(oldPorts) !== JSON.stringify(newPorts))) {
oldPorts = newPorts;
await this._proxy.$onFoundNewCandidates(oldPorts);
}
await (new Promise<void>(resolve => setTimeout(() => resolve(), this.calculateDelay(movingAverage.value))));
}
}
@@ -89,15 +199,18 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
if (provider) {
if (provider.showCandidatePort) {
this._showCandidatePort = provider.showCandidatePort;
await this._proxy.$setCandidateFilter();
}
if (provider.tunnelFactory) {
this._forwardPortProvider = provider.tunnelFactory;
await this._proxy.$setTunnelProvider();
await this._proxy.$setTunnelProvider(provider.tunnelFeatures ?? {
elevation: false,
public: false
});
}
} else {
this._forwardPortProvider = undefined;
}
await this._proxy.$tunnelServiceReady();
return toDisposable(() => {
this._forwardPortProvider = undefined;
});
@@ -110,7 +223,7 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
if (silent) {
hostMap.get(remote.port)!.disposeListener.dispose();
}
hostMap.get(remote.port)!.tunnel.dispose();
await hostMap.get(remote.port)!.tunnel.dispose();
hostMap.delete(remote.port);
}
}
@@ -120,31 +233,42 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
this._onDidChangeTunnels.fire();
}
$forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto> | undefined {
async $forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<TunnelDto | undefined> {
if (this._forwardPortProvider) {
const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions);
if (providedPort !== undefined) {
return asPromise(() => providedPort).then(tunnel => {
try {
this.logService.trace('$forwardPort: Getting tunnel from provider.');
const providedPort = this._forwardPortProvider(tunnelOptions, tunnelCreationOptions);
this.logService.trace('$forwardPort: Got tunnel promise from provider.');
if (providedPort !== undefined) {
const tunnel = await providedPort;
this.logService.trace('$forwardPort: Successfully awaited tunnel from provider.');
if (!this._extensionTunnels.has(tunnelOptions.remoteAddress.host)) {
this._extensionTunnels.set(tunnelOptions.remoteAddress.host, new Map());
}
const disposeListener = this._register(tunnel.onDidDispose(() => this._proxy.$closeTunnel(tunnel.remoteAddress)));
this._extensionTunnels.get(tunnelOptions.remoteAddress.host)!.set(tunnelOptions.remoteAddress.port, { tunnel, disposeListener });
return Promise.resolve(TunnelDto.fromApiTunnel(tunnel));
});
return TunnelDto.fromApiTunnel(tunnel);
} else {
this.logService.trace('$forwardPort: Tunnel is undefined');
}
} catch (e) {
this.logService.trace('$forwardPort: tunnel provider error');
}
}
return undefined;
}
async $applyCandidateFilter(candidates: CandidatePort[]): Promise<CandidatePort[]> {
const filter = await Promise.all(candidates.map(candidate => this._showCandidatePort(candidate.host, candidate.port, candidate.detail)));
return candidates.filter((candidate, index) => filter[index]);
}
async findCandidatePorts(): Promise<{ host: string, port: number, detail: string }[]> {
const ports: { host: string, port: number, detail: string }[] = [];
async findCandidatePorts(): Promise<CandidatePort[]> {
let tcp: string = '';
let tcp6: string = '';
try {
tcp = fs.readFileSync('/proc/net/tcp', 'utf8');
tcp6 = fs.readFileSync('/proc/net/tcp6', 'utf8');
tcp = await pfs.readFile('/proc/net/tcp', 'utf8');
tcp6 = await pfs.readFile('/proc/net/tcp6', 'utf8');
} catch (e) {
// File reading error. No additional handling needed.
}
@@ -154,105 +278,24 @@ export class ExtHostTunnelService extends Disposable implements IExtHostTunnelSe
});
}));
const procChildren = fs.readdirSync('/proc');
const processes: { pid: number, cwd: string, cmd: string }[] = [];
const procChildren = await pfs.readdir('/proc');
const processes: {
pid: number, cwd: string, cmd: string
}[] = [];
for (let childName of procChildren) {
try {
const pid: number = Number(childName);
const childUri = resources.joinPath(URI.file('/proc'), childName);
const childStat = fs.statSync(childUri.fsPath);
const childStat = await pfs.stat(childUri.fsPath);
if (childStat.isDirectory() && !isNaN(pid)) {
const cwd = fs.readlinkSync(resources.joinPath(childUri, 'cwd').fsPath);
const cmd = fs.readFileSync(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8');
const cwd = await promisify(fs.readlink)(resources.joinPath(childUri, 'cwd').fsPath);
const cmd = await pfs.readFile(resources.joinPath(childUri, 'cmdline').fsPath, 'utf8');
processes.push({ pid, cwd, cmd });
}
} catch (e) {
//
}
}
const connections: { socket: number, ip: string, port: number }[] = this.loadListeningPorts(tcp, tcp6);
const sockets = this.getSockets(procSockets);
const socketMap = sockets.reduce((m, socket) => {
m[socket.socket] = socket;
return m;
}, {} as Record<string, typeof sockets[0]>);
const processMap = processes.reduce((m, process) => {
m[process.pid] = process;
return m;
}, {} as Record<string, typeof processes[0]>);
connections.filter((connection => socketMap[connection.socket])).forEach(({ socket, ip, port }) => {
const command = processMap[socketMap[socket].pid].cmd;
if (!command.match(/.*\.vscode-server-[a-zA-Z]+\/bin.*/) && (command.indexOf('out/vs/server/main.js') === -1)) {
ports.push({ host: ip, port, detail: processMap[socketMap[socket].pid].cmd });
}
});
return ports;
}
private getSockets(stdout: string): { pid: number, socket: number }[] {
const lines = stdout.trim().split('\n');
const mapped: { pid: number, socket: number }[] = [];
lines.forEach(line => {
const match = /\/proc\/(\d+)\/fd\/\d+ -> socket:\[(\d+)\]/.exec(line)!;
if (match && match.length >= 3) {
mapped.push({
pid: parseInt(match[1], 10),
socket: parseInt(match[2], 10)
});
}
});
return mapped;
}
private loadListeningPorts(...stdouts: string[]): { socket: number, ip: string, port: number }[] {
const table = ([] as Record<string, string>[]).concat(...stdouts.map(this.loadConnectionTable));
return [
...new Map(
table.filter(row => row.st === '0A')
.map(row => {
const address = row.local_address.split(':');
return {
socket: parseInt(row.inode, 10),
ip: this.parseIpAddress(address[0]),
port: parseInt(address[1], 16)
};
}).map(port => [port.ip + ':' + port.port, port])
).values()
];
}
private parseIpAddress(hex: string): string {
let result = '';
if (hex.length === 8) {
for (let i = hex.length - 2; i >= 0; i -= 2) {
result += parseInt(hex.substr(i, 2), 16);
if (i !== 0) {
result += '.';
}
}
} else {
for (let i = hex.length - 4; i >= 0; i -= 4) {
result += parseInt(hex.substr(i, 4), 16).toString(16);
if (i !== 0) {
result += ':';
}
}
}
return result;
}
private loadConnectionTable(stdout: string): Record<string, string>[] {
const lines = stdout.trim().split('\n');
const names = lines.shift()!.trim().split(/\s+/)
.filter(name => name !== 'rx_queue' && name !== 'tm->when');
const table = lines.map(line => line.trim().split(/\s+/).reduce((obj, value, i) => {
obj[names[i] || i] = value;
return obj;
}, {} as Record<string, string>));
return table;
return findPorts(tcp, tcp6, procSockets, processes);
}
}

View File

@@ -8,10 +8,38 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost
import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { URI } from 'vs/base/common/uri';
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionIdentifier, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtensionRuntime } from 'vs/workbench/api/common/extHostTypes';
import { timeout } from 'vs/base/common/async';
namespace TrustedFunction {
// workaround a chrome issue not allowing to create new functions
// see https://github.com/w3c/webappsec-trusted-types/wiki/Trusted-Types-for-function-constructor
const ttpTrustedFunction = self.trustedTypes?.createPolicy('TrustedFunctionWorkaround', {
createScript: (_, ...args: string[]) => {
args.forEach((arg) => {
if (!self.trustedTypes?.isScript(arg)) {
throw new Error('TrustedScripts only, please');
}
});
// NOTE: This is insecure without parsing the arguments and body,
// Malicious inputs can escape the function body and execute immediately!
const fnArgs = args.slice(0, -1).join(',');
const fnBody = args.pop()!.toString();
const body = `(function anonymous(${fnArgs}) {\n${fnBody}\n})`;
return body;
}
});
export function create(...args: string[]): Function {
if (!ttpTrustedFunction) {
return new Function(...args);
}
return self.eval(ttpTrustedFunction.createScript('', ...args) as unknown as string);
}
}
class WorkerRequireInterceptor extends RequireInterceptor {
_installInterceptor() { }
@@ -35,6 +63,8 @@ class WorkerRequireInterceptor extends RequireInterceptor {
export class ExtHostExtensionService extends AbstractExtHostExtensionService {
readonly extensionRuntime = ExtensionRuntime.Webworker;
private static _ttpExtensionScripts = self.trustedTypes?.createPolicy('ExtensionScripts', { createScript: source => source });
private _fakeModules?: WorkerRequireInterceptor;
protected async _beforeAlmostReadyToRunExtensions(): Promise<void> {
@@ -42,6 +72,8 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
const apiFactory = this._instaService.invokeFunction(createApiFactoryAndRegisterActors);
this._fakeModules = this._instaService.createInstance(WorkerRequireInterceptor, apiFactory, this._registry);
await this._fakeModules.install();
performance.mark('code/extHost/didInitAPI');
await this._waitForDebuggerAttachment();
}
@@ -49,10 +81,16 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
return extensionDescription.browser;
}
protected async _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
protected async _loadCommonJSModule<T>(extensionId: ExtensionIdentifier | null, module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
module = module.with({ path: ensureSuffix(module.path, '.js') });
if (extensionId) {
performance.mark(`code/extHost/willFetchExtensionCode/${extensionId.value}`);
}
const response = await fetch(module.toString(true));
if (extensionId) {
performance.mark(`code/extHost/didFetchExtensionCode/${extensionId.value}`);
}
if (response.status !== 200) {
throw new Error(response.statusText);
@@ -63,7 +101,25 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
// Here we append #vscode-extension to serve as a marker, such that source maps
// can be adjusted for the extra wrapping function.
const sourceURL = `${module.toString(true)}#vscode-extension`;
const initFn = new Function('module', 'exports', 'require', `${source}\n//# sourceURL=${sourceURL}`);
const fullSource = `${source}\n//# sourceURL=${sourceURL}`;
let initFn: Function;
try {
initFn = TrustedFunction.create(
ExtHostExtensionService._ttpExtensionScripts?.createScript('module') as unknown as string ?? 'module',
ExtHostExtensionService._ttpExtensionScripts?.createScript('exports') as unknown as string ?? 'exports',
ExtHostExtensionService._ttpExtensionScripts?.createScript('require') as unknown as string ?? 'require',
ExtHostExtensionService._ttpExtensionScripts?.createScript(fullSource) as unknown as string ?? fullSource
);
} catch (err) {
if (extensionId) {
console.error(`Loading code for extension ${extensionId.value} failed: ${err.message}`);
} else {
console.error(`Loading code failed: ${err.message}`);
}
console.error(`${module.toString(true)}${typeof err.line === 'number' ? ` line ${err.line}` : ''}${typeof err.column === 'number' ? ` column ${err.column}` : ''}`);
console.error(err);
throw err;
}
// define commonjs globals: `module`, `exports`, and `require`
const _exports = {};
@@ -78,9 +134,15 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
try {
activationTimesBuilder.codeLoadingStart();
if (extensionId) {
performance.mark(`code/extHost/willLoadExtensionCode/${extensionId.value}`);
}
initFn(_module, _exports, _require);
return <T>(_module.exports !== _exports ? _module.exports : _exports);
} finally {
if (extensionId) {
performance.mark(`code/extHost/didLoadExtensionCode/${extensionId.value}`);
}
activationTimesBuilder.codeLoadingStop();
}
}

View File

@@ -8,25 +8,21 @@ import { Registry } from 'vs/platform/registry/common/platform';
import { Action } from 'vs/base/common/actions';
import { SyncActionDescriptor, MenuId, MenuRegistry, registerAction2, Action2 } from 'vs/platform/actions/common/actions';
import { IWorkbenchActionRegistry, Extensions as WorkbenchExtensions, CATEGORIES } from 'vs/workbench/common/actions';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IWorkbenchLayoutService, Parts, Position } from 'vs/workbench/services/layout/browser/layoutService';
import { IEditorGroupsService, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { getMenuBarVisibility } from 'vs/platform/windows/common/windows';
import { isWindows, isLinux, isWeb } from 'vs/base/common/platform';
import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { InEditorZenModeContext, IsCenteredLayoutContext, EditorAreaVisibleContext } from 'vs/workbench/common/editor';
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { SideBarVisibleContext } from 'vs/workbench/common/viewlet';
import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor } from 'vs/workbench/common/views';
import { IViewDescriptorService, IViewsService, FocusedViewContext, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService';
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
import { Codicon } from 'vs/base/common/codicons';
const registry = Registry.as<IWorkbenchActionRegistry>(WorkbenchExtensions.WorkbenchActions);
@@ -125,54 +121,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
order: 3
});
// --- Toggle Editor Layout
export class ToggleEditorLayoutAction extends Action {
static readonly ID = 'workbench.action.toggleEditorGroupLayout';
static readonly LABEL = nls.localize('flipLayout', "Toggle Vertical/Horizontal Editor Layout");
private readonly toDispose = this._register(new DisposableStore());
constructor(
id: string,
label: string,
@IEditorGroupsService private readonly editorGroupService: IEditorGroupsService
) {
super(id, label);
this.class = Codicon.editorLayout.classNames;
this.updateEnablement();
this.registerListeners();
}
private registerListeners(): void {
this.toDispose.add(this.editorGroupService.onDidAddGroup(() => this.updateEnablement()));
this.toDispose.add(this.editorGroupService.onDidRemoveGroup(() => this.updateEnablement()));
}
private updateEnablement(): void {
this.enabled = this.editorGroupService.count > 1;
}
async run(): Promise<void> {
const newOrientation = (this.editorGroupService.orientation === GroupOrientation.VERTICAL) ? GroupOrientation.HORIZONTAL : GroupOrientation.VERTICAL;
this.editorGroupService.setGroupOrientation(newOrientation);
}
}
registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleEditorLayoutAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_0, mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_0 } }), 'View: Toggle Vertical/Horizontal Editor Layout', CATEGORIES.View.value);
MenuRegistry.appendMenuItem(MenuId.MenubarLayoutMenu, {
group: 'z_flip',
command: {
id: ToggleEditorLayoutAction.ID,
title: nls.localize({ key: 'miToggleEditorLayout', comment: ['&& denotes a mnemonic'] }, "Flip &&Layout")
},
order: 1
});
// --- Toggle Sidebar Position
export class ToggleSidebarPositionAction extends Action {
@@ -203,7 +151,64 @@ export class ToggleSidebarPositionAction extends Action {
}
}
registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarPositionAction), 'View: Toggle Side Bar Position', CATEGORIES.View.value);
registerAction2(class extends Action2 {
constructor() {
super({
id: ToggleSidebarPositionAction.ID,
title: { value: nls.localize('toggleSidebarPosition', "Toggle Side Bar Position"), original: 'Toggle Side Bar Position' },
category: CATEGORIES.View,
f1: true
});
}
run(accessor: ServicesAccessor) {
accessor.get(IInstantiationService).createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.LABEL).run();
}
});
MenuRegistry.appendMenuItems([{
id: MenuId.ViewContainerTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: nls.localize('move sidebar right', "Move Side Bar Right")
},
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 1
}
}, {
id: MenuId.ViewTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: nls.localize('move sidebar right', "Move Side Bar Right")
},
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 1
}
}, {
id: MenuId.ViewContainerTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: nls.localize('move sidebar left', "Move Side Bar Left")
},
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 1
}
}, {
id: MenuId.ViewTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: nls.localize('move sidebar left', "Move Side Bar Left")
},
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 1
}
}]);
MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
group: '3_workbench_layout_move',
@@ -256,27 +261,6 @@ MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
order: 5
});
export class ToggleSidebarVisibilityAction extends Action {
static readonly ID = 'workbench.action.toggleSidebarVisibility';
static readonly LABEL = nls.localize('toggleSidebar', "Toggle Side Bar Visibility");
constructor(
id: string,
label: string,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService
) {
super(id, label);
}
async run(): Promise<void> {
const hideSidebar = this.layoutService.isVisible(Parts.SIDEBAR_PART);
this.layoutService.setSideBarHidden(hideSidebar);
}
}
registry.registerWorkbenchAction(SyncActionDescriptor.from(ToggleSidebarVisibilityAction, { primary: KeyMod.CtrlCmd | KeyCode.KEY_B }), 'View: Toggle Side Bar Visibility', CATEGORIES.View.value);
MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
group: '2_appearance',
title: nls.localize({ key: 'miAppearance', comment: ['&& denotes a mnemonic'] }, "&&Appearance"),
@@ -284,10 +268,53 @@ MenuRegistry.appendMenuItem(MenuId.MenubarViewMenu, {
order: 1
});
export const TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID = 'workbench.action.toggleSidebarVisibility';
registerAction2(class extends Action2 {
constructor() {
super({
id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID,
title: { value: nls.localize('toggleSidebar', "Toggle Side Bar Visibility"), original: 'Toggle Side Bar Visibility' },
category: CATEGORIES.View,
f1: true,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.CtrlCmd | KeyCode.KEY_B
}
});
}
run(accessor: ServicesAccessor) {
const layoutService = accessor.get(IWorkbenchLayoutService);
layoutService.setSideBarHidden(layoutService.isVisible(Parts.SIDEBAR_PART));
}
});
MenuRegistry.appendMenuItems([{
id: MenuId.ViewContainerTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID,
title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"),
},
when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewContainerLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 2
}
}, {
id: MenuId.ViewTitleContext,
item: {
group: '3_workbench_layout_move',
command: {
id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID,
title: nls.localize('compositePart.hideSideBarLabel', "Hide Side Bar"),
},
when: ContextKeyExpr.and(SideBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.Sidebar))),
order: 2
}
}]);
MenuRegistry.appendMenuItem(MenuId.MenubarAppearanceMenu, {
group: '2_workbench_layout',
command: {
id: ToggleSidebarVisibilityAction.ID,
id: TOGGLE_SIDEBAR_VISIBILITY_ACTION_ID,
title: nls.localize({ key: 'miShowSidebar', comment: ['&& denotes a mnemonic'] }, "Show &&Side Bar"),
toggled: SideBarVisibleContext
},
@@ -413,32 +440,16 @@ export class ToggleMenuBarAction extends Action {
static readonly ID = 'workbench.action.toggleMenuBar';
static readonly LABEL = nls.localize('toggleMenuBar', "Toggle Menu Bar");
private static readonly menuBarVisibilityKey = 'window.menuBarVisibility';
constructor(
id: string,
label: string,
@IConfigurationService private readonly configurationService: IConfigurationService
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService
) {
super(id, label);
}
run(): Promise<void> {
let currentVisibilityValue = getMenuBarVisibility(this.configurationService);
if (typeof currentVisibilityValue !== 'string') {
currentVisibilityValue = 'default';
}
let newVisibilityValue: string;
if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') {
newVisibilityValue = 'toggle';
} else if (currentVisibilityValue === 'compact') {
newVisibilityValue = 'hidden';
} else {
newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default';
}
return this.configurationService.updateValue(ToggleMenuBarAction.menuBarVisibilityKey, newVisibilityValue, ConfigurationTarget.USER);
async run(): Promise<void> {
this.layoutService.toggleMenuBar();
}
}
@@ -479,19 +490,18 @@ export class ResetViewLocationsAction extends Action {
registry.registerWorkbenchAction(SyncActionDescriptor.from(ResetViewLocationsAction), 'View: Reset View Locations', CATEGORIES.View.value);
// --- Toggle View with Command
export abstract class ToggleViewAction extends Action {
export class ToggleViewAction extends Action {
constructor(
id: string,
label: string,
private readonly viewId: string,
protected viewsService: IViewsService,
protected viewDescriptorService: IViewDescriptorService,
protected contextKeyService: IContextKeyService,
private layoutService: IWorkbenchLayoutService,
cssClass?: string
@IViewsService protected viewsService: IViewsService,
@IViewDescriptorService protected viewDescriptorService: IViewDescriptorService,
@IContextKeyService protected contextKeyService: IContextKeyService,
@IWorkbenchLayoutService private layoutService: IWorkbenchLayoutService,
) {
super(id, label, cssClass);
super(id, label);
}
async run(): Promise<void> {

View File

@@ -17,7 +17,6 @@ import { IWorkbenchActionRegistry, Extensions, CATEGORIES } from 'vs/workbench/c
import { Direction } from 'vs/base/browser/ui/grid/grid';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { isAncestor } from 'vs/base/browser/dom';
abstract class BaseNavigationAction extends Action {
@@ -215,7 +214,8 @@ function findVisibleNeighbour(layoutService: IWorkbenchLayoutService, part: Part
}
function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorService: IEditorService, next: boolean): void {
const currentlyFocusedPart = isActiveElementInNotebookEditor(editorService) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.EDITOR_PART) ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART :
const editorFocused = editorService.activeEditorPane?.hasFocus();
const currentlyFocusedPart = editorFocused ? Parts.EDITOR_PART : layoutService.hasFocus(Parts.ACTIVITYBAR_PART) ? Parts.ACTIVITYBAR_PART :
layoutService.hasFocus(Parts.STATUSBAR_PART) ? Parts.STATUSBAR_PART : layoutService.hasFocus(Parts.SIDEBAR_PART) ? Parts.SIDEBAR_PART : layoutService.hasFocus(Parts.PANEL_PART) ? Parts.PANEL_PART : undefined;
let partToFocus = Parts.EDITOR_PART;
if (currentlyFocusedPart) {
@@ -225,17 +225,6 @@ function focusNextOrPreviousPart(layoutService: IWorkbenchLayoutService, editorS
layoutService.focusPart(partToFocus);
}
function isActiveElementInNotebookEditor(editorService: IEditorService): boolean {
const activeEditorPane = editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined;
if (activeEditorPane?.isNotebookEditor) {
const control = editorService.activeEditorPane?.getControl() as { getDomNode(): HTMLElement; getOverflowContainerDomNode(): HTMLElement; };
const activeElement = document.activeElement;
return isAncestor(activeElement, control.getDomNode()) || isAncestor(activeElement, control.getOverflowContainerDomNode());
}
return false;
}
export class FocusNextPart extends Action {
static readonly ID = 'workbench.action.focusNextPart';
static readonly LABEL = nls.localize('focusNextPart', "Focus Next Part");

View File

@@ -76,23 +76,26 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo
// Context menu support in input/textarea
this.layoutService.container.addEventListener('contextmenu', e => this.onContextMenu(e));
}
private onContextMenu(e: MouseEvent): void {
if (e.target instanceof HTMLElement) {
const target = <HTMLElement>e.target;
if (target.nodeName && (target.nodeName.toLowerCase() === 'input' || target.nodeName.toLowerCase() === 'textarea')) {
EventHelper.stop(e, true);
this.contextMenuService.showContextMenu({
getAnchor: () => e,
getActions: () => this.textInputActions,
getActionsContext: () => target,
onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948
});
}
if (e.defaultPrevented) {
return; // make sure to not show these actions by accident if component indicated to prevent
}
const target = e.target;
if (!(target instanceof HTMLElement) || (target.nodeName.toLowerCase() !== 'input' && target.nodeName.toLowerCase() !== 'textarea')) {
return; // only for inputs or textareas
}
EventHelper.stop(e, true);
this.contextMenuService.showContextMenu({
getAnchor: () => e,
getActions: () => this.textInputActions,
getActionsContext: () => target,
onHide: () => target.focus() // fixes https://github.com/microsoft/vscode/issues/52948
});
}
}

View File

@@ -33,7 +33,7 @@ import { ResourceMap } from 'vs/base/common/map';
import { Codicon } from 'vs/base/common/codicons';
import { isHTMLElement } from 'vs/base/browser/dom';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export const inRecentFilesPickerContextKey = 'inRecentFilesPicker';
@@ -49,12 +49,17 @@ abstract class BaseOpenRecentAction extends Action {
tooltip: nls.localize('remove', "Remove from Recently Opened")
};
private readonly dirtyRecentlyOpened: IQuickInputButton = {
private readonly dirtyRecentlyOpenedFolder: IQuickInputButton = {
iconClass: 'dirty-workspace ' + Codicon.closeDirty.classNames,
tooltip: nls.localize('dirtyRecentlyOpened', "Workspace With Dirty Files"),
tooltip: nls.localize('dirtyRecentlyOpenedFolder', "Folder With Unsaved Files"),
alwaysVisible: true
};
private readonly dirtyRecentlyOpenedWorkspace: IQuickInputButton = {
...this.dirtyRecentlyOpenedFolder,
tooltip: nls.localize('dirtyRecentlyOpenedWorkspace', "Workspace With Unsaved Files"),
};
constructor(
id: string,
label: string,
@@ -77,7 +82,9 @@ abstract class BaseOpenRecentAction extends Action {
const recentlyOpened = await this.workspacesService.getRecentlyOpened();
const dirtyWorkspacesAndFolders = await this.workspacesService.getDirtyWorkspaces();
// Identify all folders and workspaces with dirty files
let hasWorkspaces = false;
// Identify all folders and workspaces with unsaved files
const dirtyFolders = new ResourceMap<boolean>();
const dirtyWorkspaces = new ResourceMap<IWorkspaceIdentifier>();
for (const dirtyWorkspace of dirtyWorkspacesAndFolders) {
@@ -85,6 +92,7 @@ abstract class BaseOpenRecentAction extends Action {
dirtyFolders.set(dirtyWorkspace, true);
} else {
dirtyWorkspaces.set(dirtyWorkspace.configPath, dirtyWorkspace);
hasWorkspaces = true;
}
}
@@ -96,6 +104,7 @@ abstract class BaseOpenRecentAction extends Action {
recentFolders.set(recent.folderUri, true);
} else {
recentWorkspaces.set(recent.workspace.configPath, recent.workspace);
hasWorkspaces = true;
}
}
@@ -124,7 +133,7 @@ abstract class BaseOpenRecentAction extends Action {
let keyMods: IKeyMods | undefined;
const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('workspaces', "workspaces") };
const workspaceSeparator: IQuickPickSeparator = { type: 'separator', label: hasWorkspaces ? nls.localize('workspacesAndFolders', "folders & workspaces") : nls.localize('folders', "folders") };
const fileSeparator: IQuickPickSeparator = { type: 'separator', label: nls.localize('files', "files") };
const picks = [workspaceSeparator, ...workspacePicks, fileSeparator, ...filePicks];
@@ -143,13 +152,14 @@ abstract class BaseOpenRecentAction extends Action {
context.removeItem();
}
// Dirty Workspace
else if (context.button === this.dirtyRecentlyOpened) {
// Dirty Folder/Workspace
else if (context.button === this.dirtyRecentlyOpenedFolder || context.button === this.dirtyRecentlyOpenedWorkspace) {
const isDirtyWorkspace = context.button === this.dirtyRecentlyOpenedWorkspace;
const result = await this.dialogService.confirm({
type: 'question',
title: nls.localize('dirtyWorkspace', "Workspace with Dirty Files"),
message: nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the dirty files?"),
detail: nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with dirty files cannot be removed until all dirty files have been saved or reverted.")
title: isDirtyWorkspace ? nls.localize('dirtyWorkspace', "Workspace with Unsaved Files") : nls.localize('dirtyFolder', "Folder with Unsaved Files"),
message: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirm', "Do you want to open the workspace to review the unsaved files?") : nls.localize('dirtyFolderConfirm', "Do you want to open the folder to review the unsaved files?"),
detail: isDirtyWorkspace ? nls.localize('dirtyWorkspaceConfirmDetail', "Workspaces with unsaved files cannot be removed until all unsaved files have been saved or reverted.") : nls.localize('dirtyFolderConfirmDetail', "Folders with unsaved files cannot be removed until all unsaved files have been saved or reverted.")
});
if (result.confirmed) {
@@ -170,6 +180,7 @@ abstract class BaseOpenRecentAction extends Action {
let iconClasses: string[];
let fullLabel: string | undefined;
let resource: URI | undefined;
let isWorkspace = false;
// Folder
if (isRecentFolder(recent)) {
@@ -185,6 +196,7 @@ abstract class BaseOpenRecentAction extends Action {
iconClasses = getIconClasses(this.modelService, this.modeService, resource, FileKind.ROOT_FOLDER);
openable = { workspaceUri: resource };
fullLabel = recent.label || this.labelService.getWorkspaceLabel(recent.workspace, { verbose: true });
isWorkspace = true;
}
// File
@@ -200,9 +212,9 @@ abstract class BaseOpenRecentAction extends Action {
return {
iconClasses,
label: name,
ariaLabel: isDirty ? nls.localize('recentDirtyAriaLabel', "{0}, dirty workspace", name) : name,
ariaLabel: isDirty ? isWorkspace ? nls.localize('recentDirtyWorkspaceAriaLabel', "{0}, workspace with unsaved changes", name) : nls.localize('recentDirtyFolderAriaLabel', "{0}, folder with unsaved changes", name) : name,
description: parentPath,
buttons: isDirty ? [this.dirtyRecentlyOpened] : [this.removeFromRecentlyOpened],
buttons: isDirty ? [isWorkspace ? this.dirtyRecentlyOpenedWorkspace : this.dirtyRecentlyOpenedFolder] : [this.removeFromRecentlyOpened],
openable,
resource
};
@@ -405,7 +417,7 @@ CommandsRegistry.registerCommand('workbench.action.toggleConfirmBeforeClose', ac
const configurationService = accessor.get(IConfigurationService);
const setting = configurationService.inspect<'always' | 'keyboardOnly' | 'never'>('window.confirmBeforeClose').userValue;
return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never', ConfigurationTarget.USER);
return configurationService.updateValue('window.confirmBeforeClose', setting === 'never' ? 'keyboardOnly' : 'never');
});
// --- Menu Registration

View File

@@ -114,7 +114,7 @@ export class CloseWorkspaceAction extends Action {
async run(): Promise<void> {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
this.notificationService.info(nls.localize('noWorkspaceOpened', "There is currently no workspace opened in this instance to close."));
this.notificationService.info(nls.localize('noWorkspaceOrFolderOpened', "There is currently no workspace or folder opened in this instance to close."));
return;
}
@@ -225,7 +225,7 @@ export class SaveWorkspaceAsAction extends Action {
export class DuplicateWorkspaceInNewWindowAction extends Action {
static readonly ID = 'workbench.action.duplicateWorkspaceInNewWindow';
static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate Workspace in New Window");
static readonly LABEL = nls.localize('duplicateWorkspaceInNewWindow', "Duplicate As Workspace in New Window");
constructor(
id: string,
@@ -259,7 +259,7 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(AddRootFolderAction),
registry.registerWorkbenchAction(SyncActionDescriptor.from(GlobalRemoveRootFolderAction), 'Workspaces: Remove Folder from Workspace...', workspacesCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(CloseWorkspaceAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.KEY_F) }), 'Workspaces: Close Workspace', workspacesCategory, EmptyWorkspaceSupportContext);
registry.registerWorkbenchAction(SyncActionDescriptor.from(SaveWorkspaceAsAction), 'Workspaces: Save Workspace As...', workspacesCategory, EmptyWorkspaceSupportContext);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate Workspace in New Window', workspacesCategory);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateWorkspaceInNewWindowAction), 'Workspaces: Duplicate As Workspace in New Window', workspacesCategory);
// --- Menu Registration

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Widget } from 'vs/base/browser/ui/widget';
import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { IOverlayWidget, ICodeEditor, IOverlayWidgetPosition, OverlayWidgetPositionPreference, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { Emitter } from 'vs/base/common/event';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
@@ -15,11 +15,121 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { IHostService } from 'vs/workbench/services/host/browser/host';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { Disposable, DisposableStore, dispose } from 'vs/base/common/lifecycle';
import { localize } from 'vs/nls';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { isEqual } from 'vs/base/common/resources';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IRange } from 'vs/editor/common/core/range';
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
export interface IRangeHighlightDecoration {
resource: URI;
range: IRange;
isWholeLine?: boolean;
}
export class RangeHighlightDecorations extends Disposable {
private readonly _onHighlightRemoved = this._register(new Emitter<void>());
readonly onHighlightRemoved = this._onHighlightRemoved.event;
private rangeHighlightDecorationId: string | null = null;
private editor: ICodeEditor | null = null;
private readonly editorDisposables = this._register(new DisposableStore());
constructor(@IEditorService private readonly editorService: IEditorService) {
super();
}
removeHighlightRange() {
if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) {
this.editor.deltaDecorations([this.rangeHighlightDecorationId], []);
this._onHighlightRemoved.fire();
}
this.rangeHighlightDecorationId = null;
}
highlightRange(range: IRangeHighlightDecoration, editor?: any) {
editor = editor ?? this.getEditor(range);
if (isCodeEditor(editor)) {
this.doHighlightRange(editor, range);
} else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) {
this.doHighlightRange(editor.activeCodeEditor, range);
}
}
private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) {
this.removeHighlightRange();
editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => {
this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine));
});
this.setEditor(editor);
}
private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined {
const activeEditor = this.editorService.activeEditor;
const resource = activeEditor && activeEditor.resource;
if (resource && isEqual(resource, resourceRange.resource)) {
return this.editorService.activeTextEditorControl as ICodeEditor;
}
return undefined;
}
private setEditor(editor: ICodeEditor) {
if (this.editor !== editor) {
this.editorDisposables.clear();
this.editor = editor;
this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
if (
e.reason === CursorChangeReason.NotSet
|| e.reason === CursorChangeReason.Explicit
|| e.reason === CursorChangeReason.Undo
|| e.reason === CursorChangeReason.Redo
) {
this.removeHighlightRange();
}
}));
this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); }));
this.editorDisposables.add(this.editor.onDidDispose(() => {
this.removeHighlightRange();
this.editor = null;
}));
}
}
private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'rangeHighlight',
isWholeLine: true
});
private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'rangeHighlight'
});
private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions {
return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT);
}
dispose() {
super.dispose();
if (this.editor && this.editor.getModel()) {
this.removeHighlightRange();
this.editor = null;
}
}
}
export class FloatingClickWidget extends Widget implements IOverlayWidget {

View File

@@ -56,15 +56,26 @@ export abstract class Composite extends Component implements IComposite {
return this._onDidBlur.event;
}
private _hasFocus = false;
hasFocus(): boolean {
return this._hasFocus;
}
private registerFocusTrackEvents(): { onDidFocus: Emitter<void>, onDidBlur: Emitter<void> } {
const container = assertIsDefined(this.getContainer());
const focusTracker = this._register(trackFocus(container));
const onDidFocus = this._onDidFocus = this._register(new Emitter<void>());
this._register(focusTracker.onDidFocus(() => onDidFocus.fire()));
this._register(focusTracker.onDidFocus(() => {
this._hasFocus = true;
onDidFocus.fire();
}));
const onDidBlur = this._onDidBlur = this._register(new Emitter<void>());
this._register(focusTracker.onDidBlur(() => onDidBlur.fire()));
this._register(focusTracker.onDidBlur(() => {
this._hasFocus = false;
onDidBlur.fire();
}));
return { onDidFocus, onDidBlur };
}
@@ -234,7 +245,6 @@ export abstract class CompositeDescriptor<T extends Composite> {
readonly cssClass?: string,
readonly order?: number,
readonly requestedIndex?: number,
readonly keybindingId?: string,
) { }
instantiate(instantiationService: IInstantiationService): T {

View File

@@ -17,7 +17,7 @@ import { WorkbenchState, IWorkspaceContextService } from 'vs/platform/workspace/
import { SideBarVisibleContext } from 'vs/workbench/common/viewlet';
import { IWorkbenchLayoutService, Parts, positionToString } from 'vs/workbench/services/layout/browser/layoutService';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { PanelPositionContext } from 'vs/workbench/common/panel';
import { PanelMaximizedContext, PanelPositionContext, PanelVisibleContext } from 'vs/workbench/common/panel';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService';
import { isNative } from 'vs/base/common/platform';
@@ -64,6 +64,8 @@ export class WorkbenchContextKeysHandler extends Disposable {
private sideBarVisibleContext: IContextKey<boolean>;
private editorAreaVisibleContext: IContextKey<boolean>;
private panelPositionContext: IContextKey<string>;
private panelVisibleContext: IContextKey<boolean>;
private panelMaximizedContext: IContextKey<boolean>;
constructor(
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@@ -146,9 +148,13 @@ export class WorkbenchContextKeysHandler extends Disposable {
// Sidebar
this.sideBarVisibleContext = SideBarVisibleContext.bindTo(this.contextKeyService);
// Panel Position
// Panel
this.panelPositionContext = PanelPositionContext.bindTo(this.contextKeyService);
this.panelPositionContext.set(positionToString(this.layoutService.getPanelPosition()));
this.panelVisibleContext = PanelVisibleContext.bindTo(this.contextKeyService);
this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART));
this.panelMaximizedContext = PanelMaximizedContext.bindTo(this.contextKeyService);
this.panelMaximizedContext.set(this.layoutService.isPanelMaximized());
this.registerListeners();
}
@@ -182,7 +188,11 @@ export class WorkbenchContextKeysHandler extends Disposable {
this._register(this.viewletService.onDidViewletClose(() => this.updateSideBarContextKeys()));
this._register(this.viewletService.onDidViewletOpen(() => this.updateSideBarContextKeys()));
this._register(this.layoutService.onPartVisibilityChange(() => this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART))));
this._register(this.layoutService.onPartVisibilityChange(() => {
this.editorAreaVisibleContext.set(this.layoutService.isVisible(Parts.EDITOR_PART));
this.panelVisibleContext.set(this.layoutService.isVisible(Parts.PANEL_PART));
this.panelMaximizedContext.set(this.layoutService.isPanelMaximized());
}));
this._register(this.workingCopyService.onDidChangeDirty(workingCopy => this.dirtyWorkingCopiesContext.set(workingCopy.isDirty() || this.workingCopyService.hasDirty)));
}

View File

@@ -737,7 +737,7 @@ export class CompositeDragAndDropObserver extends Disposable {
if (callbacks.onDragEnd) {
this._onDragEnd.event(e => {
callbacks.onDragEnd!(e);
});
}, this, disposableStore);
}
return this._register(disposableStore);
}

View File

@@ -12,7 +12,6 @@ import { insert } from 'vs/base/common/arrays';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
export interface IEditorDescriptor {
getId(): string;
getName(): string;

View File

@@ -100,6 +100,10 @@ export const DEFAULT_LABELS_CONTAINER: IResourceLabelsContainer = {
};
export class ResourceLabels extends Disposable {
private _onDidChangeDecorations = this._register(new Emitter<void>());
readonly onDidChangeDecorations = this._onDidChangeDecorations.event;
private widgets: ResourceLabelWidget[] = [];
private labels: IResourceLabel[] = [];
@@ -148,7 +152,18 @@ export class ResourceLabels extends Disposable {
}));
// notify when file decoration changes
this._register(this.decorationsService.onDidChangeDecorations(e => this.widgets.forEach(widget => widget.notifyFileDecorationsChanges(e))));
this._register(this.decorationsService.onDidChangeDecorations(e => {
let notifyDidChangeDecorations = false;
this.widgets.forEach(widget => {
if (widget.notifyFileDecorationsChanges(e)) {
notifyDidChangeDecorations = true;
}
});
if (notifyDidChangeDecorations) {
this._onDidChangeDecorations.fire();
}
}));
// notify when theme changes
this._register(this.themeService.onDidColorThemeChange(() => this.widgets.forEach(widget => widget.notifyThemeChange())));
@@ -311,19 +326,21 @@ class ResourceLabelWidget extends IconLabel {
}
}
notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): void {
notifyFileDecorationsChanges(e: IResourceDecorationChangeEvent): boolean {
if (!this.options) {
return;
return false;
}
const resource = toResource(this.label);
if (!resource) {
return;
return false;
}
if (this.options.fileDecorations && e.affectsResource(resource)) {
this.render(false);
return this.render(false);
}
return false;
}
notifyExtensionsRegistered(): void {
@@ -465,7 +482,7 @@ class ResourceLabelWidget extends IconLabel {
this.setLabel('');
}
private render(clearIconCache: boolean): void {
private render(clearIconCache: boolean): boolean {
if (this.isHidden) {
if (!this.needsRedraw) {
this.needsRedraw = clearIconCache ? Redraw.Full : Redraw.Basic;
@@ -475,7 +492,7 @@ class ResourceLabelWidget extends IconLabel {
this.needsRedraw = Redraw.Full;
}
return;
return false;
}
if (this.label) {
@@ -492,7 +509,7 @@ class ResourceLabelWidget extends IconLabel {
}
if (!this.label) {
return;
return false;
}
this.renderDisposables.clear();
@@ -558,6 +575,8 @@ class ResourceLabelWidget extends IconLabel {
this.setLabel(label || '', this.label.description, iconLabelOptions);
this._onDidRender.fire();
return true;
}
dispose(): void {

View File

@@ -78,7 +78,9 @@ enum Storage {
GRID_LAYOUT = 'workbench.grid.layout',
GRID_WIDTH = 'workbench.grid.width',
GRID_HEIGHT = 'workbench.grid.height'
GRID_HEIGHT = 'workbench.grid.height',
MENU_VISIBILITY = 'window.menuBarVisibility'
}
enum Classes {
@@ -321,9 +323,12 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
if (this.state.fullscreen && (this.state.menuBar.visibility === 'toggle' || this.state.menuBar.visibility === 'default')) {
// Propagate to grid
this.workbenchGrid.setViewVisible(this.titleBarPartView, this.isVisible(Parts.TITLEBAR_PART));
this.layout();
}
// Move layout call to any time the menubar
// is toggled to update consumers of offset
// see issue #115267
this.layout();
}
}
@@ -622,7 +627,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
return this._openedDefaultEditors;
}
private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[] } | undefined {
private getInitialFilesToOpen(): { filesToOpenOrCreate?: IPath[], filesToDiff?: IPath[]; } | undefined {
const defaultLayout = this.environmentService.options?.defaultLayout;
if (defaultLayout?.editors?.length && this.storageService.isNew(StorageScope.WORKSPACE)) {
this._openedDefaultEditors = true;
@@ -652,7 +657,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
// Restore editors
restorePromises.push((async () => {
mark('willRestoreEditors');
mark('code/willRestoreEditors');
// first ensure the editor part is restored
await this.editorGroupService.whenRestored;
@@ -669,17 +674,17 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
await this.editorService.openEditors(editors);
}
mark('didRestoreEditors');
mark('code/didRestoreEditors');
})());
// Restore default views
const restoreDefaultViewsPromise = (async () => {
if (this.state.views.defaults?.length) {
mark('willOpenDefaultViews');
mark('code/willOpenDefaultViews');
let locationsRestored: { id: string; order: number }[] = [];
let locationsRestored: { id: string; order: number; }[] = [];
const tryOpenView = (view: { id: string; order: number }): boolean => {
const tryOpenView = (view: { id: string; order: number; }): boolean => {
const location = this.viewDescriptorService.getViewLocationById(view.id);
if (location !== null) {
const container = this.viewDescriptorService.getViewContainerByViewId(view.id);
@@ -733,7 +738,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
this.state.panel.panelToRestore = locationsRestored[ViewContainerLocation.Panel].id;
}
mark('didOpenDefaultViews');
mark('code/didOpenDefaultViews');
}
})();
restorePromises.push(restoreDefaultViewsPromise);
@@ -748,14 +753,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
return;
}
mark('willRestoreViewlet');
mark('code/willRestoreViewlet');
const viewlet = await this.viewletService.openViewlet(this.state.sideBar.viewletToRestore);
if (!viewlet) {
await this.viewletService.openViewlet(this.viewDescriptorService.getDefaultViewContainer(ViewContainerLocation.Sidebar)?.id); // fallback to default viewlet as needed
}
mark('didRestoreViewlet');
mark('code/didRestoreViewlet');
})());
// Restore Panel
@@ -768,14 +773,14 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
return;
}
mark('willRestorePanel');
mark('code/willRestorePanel');
const panel = await this.panelService.openPanel(this.state.panel.panelToRestore!);
if (!panel) {
await this.panelService.openPanel(Registry.as<PanelRegistry>(PanelExtensions.Panels).getDefaultPanelId()); // fallback to default panel as needed
}
mark('didRestorePanel');
mark('code/didRestorePanel');
})());
// Restore Zen Mode
@@ -1128,7 +1133,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
[Parts.STATUSBAR_PART]: this.statusBarPartView
};
const fromJSON = ({ type }: { type: Parts }) => viewMap[type];
const fromJSON = ({ type }: { type: Parts; }) => viewMap[type];
const workbenchGrid = SerializableGrid.deserialize(
this.createGridDescriptor(),
{ fromJSON },
@@ -1410,7 +1415,29 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
// If panel part becomes visible, show last active panel or default panel
else if (!hidden && !this.panelService.getActivePanel()) {
const panelToOpen = this.panelService.getLastActivePanelId();
let panelToOpen: string | undefined = this.panelService.getLastActivePanelId();
const hasViews = (id: string): boolean => {
const viewContainer = this.viewDescriptorService.getViewContainerById(id);
if (!viewContainer) {
return false;
}
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
if (!viewContainerModel) {
return false;
}
return viewContainerModel.activeViewDescriptors.length >= 1;
};
// verify that the panel we try to open has views before we default to it
// otherwise fall back to any view that has views still refs #111463
if (!panelToOpen || !hasViews(panelToOpen)) {
panelToOpen = this.viewDescriptorService
.getViewContainersByLocation(ViewContainerLocation.Panel)
.find(viewContainer => hasViews(viewContainer.id))?.id;
}
if (panelToOpen) {
const focus = !skipLayout;
this.panelService.openPanel(panelToOpen, focus);
@@ -1529,6 +1556,24 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
return this.state.menuBar.visibility;
}
toggleMenuBar(): void {
let currentVisibilityValue = getMenuBarVisibility(this.configurationService);
if (typeof currentVisibilityValue !== 'string') {
currentVisibilityValue = 'default';
}
let newVisibilityValue: string;
if (currentVisibilityValue === 'visible' || currentVisibilityValue === 'default') {
newVisibilityValue = 'toggle';
} else if (currentVisibilityValue === 'compact') {
newVisibilityValue = 'hidden';
} else {
newVisibilityValue = (isWeb && currentVisibilityValue === 'hidden') ? 'compact' : 'default';
}
this.configurationService.updateValue(Storage.MENU_VISIBILITY, newVisibilityValue);
}
getPanelPosition(): Position {
return this.state.panel.position;
}

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IAction } from 'vs/base/common/actions';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { MenuId, IMenuService, IMenu, SubmenuItemAction, IMenuActionOptions } from 'vs/platform/actions/common/actions';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
class MenuActions extends Disposable {
private readonly menu: IMenu;
private _primaryActions: IAction[] = [];
get primaryActions() { return this._primaryActions; }
private _secondaryActions: IAction[] = [];
get secondaryActions() { return this._secondaryActions; }
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;
private disposables = this._register(new DisposableStore());
constructor(
menuId: MenuId,
private readonly options: IMenuActionOptions | undefined,
private readonly menuService: IMenuService,
private readonly contextKeyService: IContextKeyService
) {
super();
this.menu = this._register(menuService.createMenu(menuId, contextKeyService));
this._register(this.menu.onDidChange(() => this.updateActions()));
this.updateActions();
}
private updateActions(): void {
this.disposables.clear();
this._primaryActions = [];
this._secondaryActions = [];
this.disposables.add(createAndFillInActionBarActions(this.menu, this.options, { primary: this._primaryActions, secondary: this._secondaryActions }));
this.disposables.add(this.updateSubmenus([...this._primaryActions, ...this._secondaryActions], {}));
this._onDidChange.fire();
}
private updateSubmenus(actions: readonly IAction[], submenus: { [id: number]: IMenu }): IDisposable {
const disposables = new DisposableStore();
for (const action of actions) {
if (action instanceof SubmenuItemAction && !submenus[action.item.submenu.id]) {
const menu = submenus[action.item.submenu.id] = disposables.add(this.menuService.createMenu(action.item.submenu, this.contextKeyService));
disposables.add(menu.onDidChange(() => this.updateActions()));
disposables.add(this.updateSubmenus(action.actions, submenus));
}
}
return disposables;
}
}
export class CompositeMenuActions extends Disposable {
private readonly menuActions: MenuActions;
private readonly contextMenuActionsDisposable = this._register(new MutableDisposable());
private _onDidChange = this._register(new Emitter<void>());
readonly onDidChange: Event<void> = this._onDidChange.event;
constructor(
menuId: MenuId,
private readonly contextMenuId: MenuId | undefined,
private readonly options: IMenuActionOptions | undefined,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IMenuService private readonly menuService: IMenuService,
) {
super();
this.menuActions = this._register(new MenuActions(menuId, this.options, menuService, contextKeyService));
this._register(this.menuActions.onDidChange(() => this._onDidChange.fire()));
}
getPrimaryActions(): IAction[] {
return this.menuActions.primaryActions;
}
getSecondaryActions(): IAction[] {
return this.menuActions.secondaryActions;
}
getContextMenuActions(): IAction[] {
const actions: IAction[] = [];
if (this.contextMenuId) {
const menu = this.menuService.createMenu(this.contextMenuId, this.contextKeyService);
this.contextMenuActionsDisposable.value = createAndFillInActionBarActions(menu, this.options, { primary: [], secondary: actions });
menu.dispose();
}
return actions;
}
}

View File

@@ -16,13 +16,9 @@ import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace
import { ViewPaneContainer } from './parts/views/viewPaneContainer';
import { IPaneComposite } from 'vs/workbench/common/panecomposite';
import { IAction, IActionViewItem, Separator } from 'vs/base/common/actions';
import { ViewContainerMenuActions } from 'vs/workbench/browser/parts/views/viewMenuActions';
import { MenuId } from 'vs/platform/actions/common/actions';
export class PaneComposite extends Composite implements IPaneComposite {
private menuActions: ViewContainerMenuActions;
constructor(
id: string,
protected readonly viewPaneContainer: ViewPaneContainer,
@@ -35,8 +31,6 @@ export class PaneComposite extends Composite implements IPaneComposite {
@IWorkspaceContextService protected contextService: IWorkspaceContextService
) {
super(id, telemetryService, themeService, storageService);
this.menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.getId(), MenuId.ViewContainerTitleContext));
this._register(this.viewPaneContainer.onTitleAreaUpdate(() => this.updateTitleArea()));
}
@@ -71,22 +65,36 @@ export class PaneComposite extends Composite implements IPaneComposite {
getContextMenuActions(): ReadonlyArray<IAction> {
const result = [];
result.push(...this.menuActions.getContextMenuActions());
result.push(...this.viewPaneContainer.getContextMenuActions2());
if (result.length) {
const otherActions = this.viewPaneContainer.getContextMenuActions();
if (otherActions.length) {
result.push(new Separator());
result.push(...otherActions);
}
result.push(...this.viewPaneContainer.getContextMenuActions());
return result;
}
getActions(): ReadonlyArray<IAction> {
return this.viewPaneContainer.getActions();
const result = [];
result.push(...this.viewPaneContainer.getActions2());
result.push(...this.viewPaneContainer.getActions());
return result;
}
getSecondaryActions(): ReadonlyArray<IAction> {
return this.viewPaneContainer.getSecondaryActions();
const menuActions = this.viewPaneContainer.getSecondaryActions2();
const viewPaneContainerActions = this.viewPaneContainer.getSecondaryActions();
if (menuActions.length && viewPaneContainerActions.length) {
return [
...menuActions,
new Separator(),
...viewPaneContainerActions
];
}
return menuActions.length ? menuActions : viewPaneContainerActions;
}
getActionViewItem(action: IAction): IActionViewItem | undefined {

View File

@@ -6,23 +6,74 @@
import { Registry } from 'vs/platform/registry/common/platform';
import { IPanel } from 'vs/workbench/common/panel';
import { CompositeDescriptor, CompositeRegistry } from 'vs/workbench/browser/composite';
import { IConstructorSignature0, BrandedService } from 'vs/platform/instantiation/common/instantiation';
import { IConstructorSignature0, BrandedService, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { assertIsDefined } from 'vs/base/common/types';
import { PaneComposite } from 'vs/workbench/browser/panecomposite';
import { IAction, Separator } from 'vs/base/common/actions';
import { CompositeMenuActions } from 'vs/workbench/browser/menuActions';
import { MenuId } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ViewPaneContainer } from 'vs/workbench/browser/parts/views/viewPaneContainer';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
export abstract class Panel extends PaneComposite implements IPanel { }
export abstract class Panel extends PaneComposite implements IPanel {
private readonly panelActions: CompositeMenuActions;
constructor(id: string,
viewPaneContainer: ViewPaneContainer,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageService storageService: IStorageService,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IContextMenuService contextMenuService: IContextMenuService,
@IExtensionService extensionService: IExtensionService,
@IWorkspaceContextService contextService: IWorkspaceContextService,
) {
super(id, viewPaneContainer, telemetryService, storageService, instantiationService, themeService, contextMenuService, extensionService, contextService);
this.panelActions = this._register(this.instantiationService.createInstance(CompositeMenuActions, MenuId.PanelTitle, undefined, undefined));
this._register(this.panelActions.onDidChange(() => this.updateTitleArea()));
}
getActions(): ReadonlyArray<IAction> {
return [...super.getActions(), ...this.panelActions.getPrimaryActions()];
}
getSecondaryActions(): ReadonlyArray<IAction> {
return this.mergeSecondaryActions(super.getSecondaryActions(), this.panelActions.getSecondaryActions());
}
getContextMenuActions(): ReadonlyArray<IAction> {
return this.mergeSecondaryActions(super.getContextMenuActions(), this.panelActions.getContextMenuActions());
}
private mergeSecondaryActions(actions: ReadonlyArray<IAction>, panelActions: IAction[]): ReadonlyArray<IAction> {
if (panelActions.length && actions.length) {
return [
...actions,
new Separator(),
...panelActions,
];
}
return panelActions.length ? panelActions : actions;
}
}
/**
* A panel descriptor is a leightweight descriptor of a panel in the workbench.
*/
export class PanelDescriptor extends CompositeDescriptor<Panel> {
static create<Services extends BrandedService[]>(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string): PanelDescriptor {
return new PanelDescriptor(ctor as IConstructorSignature0<Panel>, id, name, cssClass, order, requestedIndex, _commandId);
static create<Services extends BrandedService[]>(ctor: { new(...services: Services): Panel }, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number): PanelDescriptor {
return new PanelDescriptor(ctor as IConstructorSignature0<Panel>, id, name, cssClass, order, requestedIndex);
}
private constructor(ctor: IConstructorSignature0<Panel>, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number, _commandId?: string) {
super(ctor, id, name, cssClass, order, requestedIndex, _commandId);
private constructor(ctor: IConstructorSignature0<Panel>, id: string, name: string, cssClass?: string, order?: number, requestedIndex?: number) {
super(ctor, id, name, cssClass, order, requestedIndex);
}
}

View File

@@ -162,7 +162,7 @@ class PartLayout {
if (this.options && this.options.hasTitle) {
titleSize = new Dimension(width, Math.min(height, PartLayout.TITLE_HEIGHT));
} else {
titleSize = new Dimension(0, 0);
titleSize = Dimension.None;
}
let contentWidth = width;

View File

@@ -4,18 +4,18 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/activityaction';
import * as nls from 'vs/nls';
import * as DOM from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { EventType, addDisposableListener, EventHelper } from 'vs/base/browser/dom';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import { Action, IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
import { Action, IAction, Separator, SubmenuAction, toAction } from 'vs/base/common/actions';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IMenuService, MenuId, IMenu, registerAction2, Action2, IAction2Options } from 'vs/platform/actions/common/actions';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { activeContrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegistry';
import { ICssStyleCollector, IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { ActivityAction, ActivityActionViewItem, ICompositeBar, ICompositeBarColors, ToggleCompositePinnedAction } from 'vs/workbench/browser/parts/compositeBarActions';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { IActivity } from 'vs/workbench/common/activity';
@@ -29,12 +29,12 @@ import { isMacintosh, isWeb } from 'vs/base/common/platform';
import { getCurrentAuthenticationSessionInfo, IAuthenticationService } from 'vs/workbench/services/authentication/browser/authenticationService';
import { AuthenticationSession } from 'vs/editor/common/modes';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IProductService } from 'vs/platform/product/common/productService';
import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { getTitleBarStyle } from 'vs/platform/windows/common/windows';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
export class ViewContainerActivityAction extends ActivityAction {
@@ -97,10 +97,10 @@ export class ViewContainerActivityAction extends ActivityAction {
private logAction(action: string) {
type ActivityBarActionClassification = {
viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
action: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
viewletId: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
action: { classification: 'SystemMetaData', purpose: 'FeatureInsight'; };
};
this.telemetryService.publicLog2<{ viewletId: String, action: String }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action });
this.telemetryService.publicLog2<{ viewletId: String, action: String; }, ActivityBarActionClassification>('activityBarAction', { viewletId: this.activity.id, action });
}
}
@@ -109,6 +109,7 @@ class MenuActivityActionViewItem extends ActivityActionViewItem {
constructor(
private readonly menuId: MenuId,
action: ActivityAction,
private contextMenuActionsProvider: () => IAction[],
colors: (theme: IColorTheme) => ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IMenuService protected readonly menuService: IMenuService,
@@ -126,30 +127,35 @@ class MenuActivityActionViewItem extends ActivityActionViewItem {
// Context menus are triggered on mouse down so that an item can be picked
// and executed with releasing the mouse over it
this._register(DOM.addDisposableListener(this.container, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
DOM.EventHelper.stop(e, true);
this._register(addDisposableListener(this.container, EventType.MOUSE_DOWN, (e: MouseEvent) => {
EventHelper.stop(e, true);
this.showContextMenu(e);
}));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_UP, (e: KeyboardEvent) => {
this._register(addDisposableListener(this.container, EventType.KEY_UP, (e: KeyboardEvent) => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
DOM.EventHelper.stop(e, true);
EventHelper.stop(e, true);
this.showContextMenu();
}
}));
this._register(DOM.addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => {
DOM.EventHelper.stop(e, true);
this._register(addDisposableListener(this.container, TouchEventType.Tap, (e: GestureEvent) => {
EventHelper.stop(e, true);
this.showContextMenu();
}));
}
protected async showContextMenu(e?: MouseEvent): Promise<void> {
private async showContextMenu(e?: MouseEvent): Promise<void> {
const disposables = new DisposableStore();
const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService));
const actions = await this.resolveActions(menu, disposables);
let actions: IAction[];
if (e?.button !== 2) {
const menu = disposables.add(this.menuService.createMenu(this.menuId, this.contextKeyService));
actions = await this.resolveMainMenuActions(menu, disposables);
} else {
actions = await this.resolveContextMenuActions(disposables);
}
const isUsingCustomMenu = isWeb || (getTitleBarStyle(this.configurationService) !== 'native' && !isMacintosh); // see #40262
const position = this.configurationService.getValue('workbench.sideBar.location');
@@ -163,13 +169,17 @@ class MenuActivityActionViewItem extends ActivityActionViewItem {
});
}
protected async resolveActions(menu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
protected async resolveMainMenuActions(menu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
const actions: IAction[] = [];
disposables.add(createAndFillInActionBarActions(menu, undefined, { primary: [], secondary: actions }));
return actions;
}
protected async resolveContextMenuActions(disposables: DisposableStore): Promise<IAction[]> {
return this.contextMenuActionsProvider();
}
}
export class HomeActivityActionViewItem extends MenuActivityActionViewItem {
@@ -179,6 +189,7 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem {
constructor(
private readonly goHomeHref: string,
action: ActivityAction,
contextMenuActionsProvider: () => IAction[],
colors: (theme: IColorTheme) => ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IMenuService menuService: IMenuService,
@@ -188,27 +199,32 @@ export class HomeActivityActionViewItem extends MenuActivityActionViewItem {
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IStorageService private readonly storageService: IStorageService
) {
super(MenuId.MenubarHomeMenu, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
super(MenuId.MenubarHomeMenu, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
}
protected async resolveActions(homeMenu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
protected async resolveMainMenuActions(homeMenu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
const actions = [];
// Go Home
actions.push(disposables.add(new Action('goHome', nls.localize('goHome', "Go Home"), undefined, true, async () => window.location.href = this.goHomeHref)));
actions.push(disposables.add(new Separator()));
actions.push(toAction({ id: 'goHome', label: localize('goHome', "Go Home"), run: () => window.location.href = this.goHomeHref }));
// Contributed
const contributedActions = await super.resolveActions(homeMenu, disposables);
actions.push(...contributedActions);
// Hide
if (contributedActions.length > 0) {
const contributedActions = await super.resolveMainMenuActions(homeMenu, disposables);
if (contributedActions.length) {
actions.push(disposables.add(new Separator()));
actions.push(...contributedActions);
}
actions.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => {
this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER);
})));
return actions;
}
protected async resolveContextMenuActions(disposables: DisposableStore): Promise<IAction[]> {
const actions = await super.resolveContextMenuActions(disposables);
actions.unshift(...[
toAction({ id: 'hideHomeButton', label: localize('hideHomeButton', "Hide Home Button"), run: () => this.storageService.store(HomeActivityActionViewItem.HOME_BAR_VISIBILITY_PREFERENCE, false, StorageScope.GLOBAL, StorageTarget.USER) }),
new Separator()
]);
return actions;
}
@@ -220,6 +236,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
constructor(
action: ActivityAction,
contextMenuActionsProvider: () => IAction[],
colors: (theme: IColorTheme) => ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IContextMenuService contextMenuService: IContextMenuService,
@@ -227,15 +244,15 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
@IContextKeyService contextKeyService: IContextKeyService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@IStorageService private readonly storageService: IStorageService,
@IProductService private readonly productService: IProductService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService
) {
super(MenuId.AccountsContext, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
super(MenuId.AccountsContext, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
}
protected async resolveActions(accountsMenu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
await super.resolveActions(accountsMenu, disposables);
protected async resolveMainMenuActions(accountsMenu: IMenu, disposables: DisposableStore): Promise<IAction[]> {
await super.resolveMainMenuActions(accountsMenu, disposables);
const otherCommands = accountsMenu.getActions();
const providers = this.authenticationService.getProviderIds();
@@ -243,7 +260,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
try {
const sessions = await this.authenticationService.getSessions(providerId);
const groupedSessions: { [label: string]: AuthenticationSession[] } = {};
const groupedSessions: { [label: string]: AuthenticationSession[]; } = {};
sessions.forEach(session => {
if (groupedSessions[session.account.label]) {
groupedSessions[session.account.label].push(session);
@@ -266,11 +283,11 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
if (sessionInfo.sessions) {
Object.keys(sessionInfo.sessions).forEach(accountName => {
const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, nls.localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => {
const manageExtensionsAction = disposables.add(new Action(`configureSessions${accountName}`, localize('manageTrustedExtensions', "Manage Trusted Extensions"), '', true, () => {
return this.authenticationService.manageTrustedExtensionsForAccount(sessionInfo.providerId, accountName);
}));
const signOutAction = disposables.add(new Action('signOut', nls.localize('signOut', "Sign Out"), '', true, () => {
const signOutAction = disposables.add(new Action('signOut', localize('signOut', "Sign Out"), '', true, () => {
return this.authenticationService.signOutOfAccount(sessionInfo.providerId, accountName);
}));
@@ -285,7 +302,7 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
menus.push(providerSubMenu);
});
} else {
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', nls.localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName)));
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerDisplayName)));
menus.push(providerUnavailableAction);
}
});
@@ -302,22 +319,26 @@ export class AccountsActivityActionViewItem extends MenuActivityActionViewItem {
}
});
if (menus.length) {
menus.push(disposables.add(new Separator()));
}
menus.push(disposables.add(new Action('hide', nls.localize('hide', "Hide"), undefined, true, async () => {
this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER);
})));
return menus;
}
protected async resolveContextMenuActions(disposables: DisposableStore): Promise<IAction[]> {
const actions = await super.resolveContextMenuActions(disposables);
actions.unshift(...[
toAction({ id: 'hideAccounts', label: localize('hideAccounts', "Hide Accounts"), run: () => this.storageService.store(AccountsActivityActionViewItem.ACCOUNTS_VISIBILITY_PREFERENCE_KEY, false, StorageScope.GLOBAL, StorageTarget.USER) }),
new Separator()
]);
return actions;
}
}
export class GlobalActivityActionViewItem extends MenuActivityActionViewItem {
constructor(
action: ActivityAction,
contextMenuActionsProvider: () => IAction[],
colors: (theme: IColorTheme) => ICompositeBarColors,
@IThemeService themeService: IThemeService,
@IMenuService menuService: IMenuService,
@@ -326,7 +347,7 @@ export class GlobalActivityActionViewItem extends MenuActivityActionViewItem {
@IConfigurationService configurationService: IConfigurationService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService
) {
super(MenuId.GlobalActivity, action, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
super(MenuId.GlobalActivity, action, contextMenuActionsProvider, colors, themeService, menuService, contextMenuService, contextKeyService, configurationService, environmentService);
}
}
@@ -379,7 +400,7 @@ registerAction2(
constructor() {
super({
id: 'workbench.action.previousSideBarView',
title: { value: nls.localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' },
title: { value: localize('previousSideBarView', "Previous Side Bar View"), original: 'Previous Side Bar View' },
category: CATEGORIES.View,
f1: true
}, -1);
@@ -392,7 +413,7 @@ registerAction2(
constructor() {
super({
id: 'workbench.action.nextSideBarView',
title: { value: nls.localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' },
title: { value: localize('nextSideBarView', "Next Side Bar View"), original: 'Next Side Bar View' },
category: CATEGORIES.View,
f1: true
}, 1);
@@ -400,7 +421,7 @@ registerAction2(
}
);
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
registerThemingParticipant((theme, collector) => {
const activityBarBackgroundColor = theme.getColor(ACTIVITY_BAR_BACKGROUND);
if (activityBarBackgroundColor) {
collector.addRule(`

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/activitybarpart';
import * as nls from 'vs/nls';
import { localize } from 'vs/nls';
import { ActionsOrientation, ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
import { GLOBAL_ACTIVITY_ID, IActivity, ACCOUNTS_ACTIVITY_ID } from 'vs/workbench/common/activity';
import { Part } from 'vs/workbench/browser/part';
@@ -13,7 +13,7 @@ import { IBadge, NumberBadge } from 'vs/workbench/services/activity/common/activ
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
import { ToggleActivityBarVisibilityAction, ToggleMenuBarAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions';
import { ToggleActivityBarVisibilityAction, ToggleSidebarPositionAction } from 'vs/workbench/browser/actions/layoutActions';
import { IThemeService, IColorTheme, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ACTIVITY_BAR_BACKGROUND, ACTIVITY_BAR_BORDER, ACTIVITY_BAR_FOREGROUND, ACTIVITY_BAR_ACTIVE_BORDER, ACTIVITY_BAR_BADGE_BACKGROUND, ACTIVITY_BAR_BADGE_FOREGROUND, ACTIVITY_BAR_INACTIVE_FOREGROUND, ACTIVITY_BAR_ACTIVE_BACKGROUND, ACTIVITY_BAR_DRAG_AND_DROP_BORDER } from 'vs/workbench/common/theme';
import { contrastBorder } from 'vs/platform/theme/common/colorRegistry';
@@ -23,33 +23,34 @@ import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ToggleCompositePinnedAction, ICompositeBarColors, ActivityAction, ICompositeActivity } from 'vs/workbench/browser/parts/compositeBarActions';
import { IViewDescriptorService, ViewContainer, TEST_VIEW_CONTAINER_ID, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views';
import { IViewDescriptorService, ViewContainer, IViewContainerModel, ViewContainerLocation, IViewsService } from 'vs/workbench/common/views';
import { IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { assertIsDefined } from 'vs/base/common/types';
import { assertIsDefined, isString } from 'vs/base/common/types';
import { IActivityBarService } from 'vs/workbench/services/activityBar/browser/activityBarService';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { Schemas } from 'vs/base/common/network';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { getMenuBarVisibility } from 'vs/platform/windows/common/windows';
import { isWeb } from 'vs/base/common/platform';
import { isNative, isWeb } from 'vs/base/common/platform';
import { Before2D } from 'vs/workbench/browser/dnd';
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
import { Action, Separator } from 'vs/base/common/actions';
import { IAction, Separator, toAction } from 'vs/base/common/actions';
import { Event } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Action2, registerAction2 } from 'vs/platform/actions/common/actions';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { StringSHA1 } from 'vs/base/common/hash';
interface IPlaceholderViewContainer {
readonly id: string;
readonly name?: string;
readonly iconUrl?: UriComponents;
readonly themeIcon?: ThemeIcon;
readonly views?: { when?: string }[];
readonly isBuiltin?: boolean;
readonly views?: { when?: string; }[];
}
interface IPinnedViewContainer {
@@ -66,12 +67,10 @@ interface ICachedViewContainer {
readonly pinned: boolean;
readonly order?: number;
visible: boolean;
views?: { when?: string }[];
isBuiltin?: boolean;
views?: { when?: string; }[];
}
const settingsViewBarIcon = registerIcon('settings-view-bar-icon', Codicon.settingsGear, nls.localize('settingsViewBarIcon', 'Settings icon in the view bar.'));
const accountsViewBarIcon = registerIcon('accounts-view-bar-icon', Codicon.account, nls.localize('accountsViewBarIcon', 'Accounts icon in the view bar.'));
export class ActivitybarPart extends Part implements IActivityBarService {
declare readonly _serviceBrand: undefined;
@@ -81,6 +80,9 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private static readonly ACTION_HEIGHT = 48;
private static readonly ACCOUNTS_ACTION_INDEX = 0;
private static readonly GEAR_ICON = registerIcon('settings-view-bar-icon', Codicon.settingsGear, localize('settingsViewBarIcon', "Settings icon in the view bar."));
private static readonly ACCOUNTS_ICON = registerIcon('accounts-view-bar-icon', Codicon.account, localize('accountsViewBarIcon', "Accounts icon in the view bar."));
//#region IView
readonly minimumWidth: number = 48;
@@ -110,12 +112,13 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private readonly accountsActivity: ICompositeActivity[] = [];
private readonly compositeActions = new Map<string, { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction }>();
private readonly compositeActions = new Map<string, { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction; }>();
private readonly viewContainerDisposables = new Map<string, IDisposable>();
private readonly keyboardNavigationDisposables = this._register(new DisposableStore());
private readonly location = ViewContainerLocation.Sidebar;
private hasExtensionsRegistered: boolean = false;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -132,14 +135,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
super(Parts.ACTIVITYBAR_PART, { hasTitle: false }, themeService, storageService, layoutService);
for (const cachedViewContainer of this.cachedViewContainers) {
if (
environmentService.remoteAuthority || // In remote window, hide activity bar entries until registered
this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer)
) {
cachedViewContainer.visible = false;
}
cachedViewContainer.visible = !this.shouldBeHidden(cachedViewContainer.id, cachedViewContainer);
}
this.compositeBar = this.createCompositeBar();
this.onDidRegisterViewContainers(this.getViewContainers());
@@ -161,53 +158,66 @@ export class ActivitybarPart extends Part implements IActivityBarService {
icon: true,
orientation: ActionsOrientation.VERTICAL,
preventLoopNavigation: true,
openComposite: (compositeId: string) => this.viewsService.openViewContainer(compositeId, true),
getActivityAction: (compositeId: string) => this.getCompositeActions(compositeId).activityAction,
getCompositePinnedAction: (compositeId: string) => this.getCompositeActions(compositeId).pinnedAction,
getOnCompositeClickAction: (compositeId: string) => new Action(compositeId, '', '', true, () => this.viewsService.isViewContainerVisible(compositeId) ? Promise.resolve(this.viewsService.closeViewContainer(compositeId)) : this.viewsService.openViewContainer(compositeId)),
getContextMenuActions: () => {
const actions = [];
openComposite: compositeId => this.viewsService.openViewContainer(compositeId, true),
getActivityAction: compositeId => this.getCompositeActions(compositeId).activityAction,
getCompositePinnedAction: compositeId => this.getCompositeActions(compositeId).pinnedAction,
getOnCompositeClickAction: compositeId => toAction({ id: compositeId, label: '', run: async () => this.viewsService.isViewContainerVisible(compositeId) ? this.viewsService.closeViewContainer(compositeId) : this.viewsService.openViewContainer(compositeId) }),
fillExtraContextMenuActions: actions => {
// Home
const topActions: IAction[] = [];
if (this.homeBarContainer) {
actions.push(new Action(
'toggleHomeBarAction',
this.homeBarVisibilityPreference ? nls.localize('hideHomeBar', "Hide Home Button") : nls.localize('showHomeBar', "Show Home Button"),
undefined,
true,
async () => { this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference; }
));
topActions.push({
id: 'toggleHomeBarAction',
label: localize('homeButton', "Home Button"),
class: undefined,
tooltip: localize('homeButton', "Home Button"),
checked: this.homeBarVisibilityPreference,
enabled: true,
run: async () => this.homeBarVisibilityPreference = !this.homeBarVisibilityPreference,
dispose: () => { }
});
}
// Menu
const menuBarVisibility = getMenuBarVisibility(this.configurationService);
if (menuBarVisibility === 'compact' || (menuBarVisibility === 'hidden' && isWeb)) {
actions.push(this.instantiationService.createInstance(ToggleMenuBarAction, ToggleMenuBarAction.ID, menuBarVisibility === 'compact' ? nls.localize('hideMenu', "Hide Menu") : nls.localize('showMenu', "Show Menu")));
topActions.push({
id: 'toggleMenuVisibility',
label: localize('menu', "Menu"),
class: undefined,
tooltip: localize('menu', "Menu"),
checked: menuBarVisibility === 'compact',
enabled: true,
run: async () => this.layoutService.toggleMenuBar(),
dispose: () => { }
});
}
if (topActions.length) {
actions.unshift(...topActions, new Separator());
}
// Accounts
actions.push(new Action(
'toggleAccountsVisibility',
this.accountsVisibilityPreference ? nls.localize('hideAccounts', "Hide Accounts") : nls.localize('showAccounts', "Show Accounts"),
undefined,
true,
async () => { this.accountsVisibilityPreference = !this.accountsVisibilityPreference; }
));
actions.push(new Separator());
actions.push({
id: 'toggleAccountsVisibility',
label: localize('accounts', "Accounts"),
class: undefined,
tooltip: localize('accounts', "Accounts"),
checked: this.accountsVisibilityPreference,
enabled: true,
run: async () => this.accountsVisibilityPreference = !this.accountsVisibilityPreference,
dispose: () => { }
});
actions.push(new Separator());
// Toggle Sidebar
actions.push(this.instantiationService.createInstance(ToggleSidebarPositionAction, ToggleSidebarPositionAction.ID, ToggleSidebarPositionAction.getLabel(this.layoutService)));
// Toggle Activity Bar
actions.push(new Action(
ToggleActivityBarVisibilityAction.ID,
nls.localize('hideActivitBar', "Hide Activity Bar"),
undefined,
true,
async () => { this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)); }
));
return actions;
actions.push(toAction({ id: ToggleActivityBarVisibilityAction.ID, label: localize('hideActivitBar', "Hide Activity Bar"), run: async () => this.instantiationService.invokeFunction(accessor => new ToggleActivityBarVisibilityAction().run(accessor)) }));
},
getContextMenuActionsForComposite: compositeId => this.getContextMenuActionsForComposite(compositeId),
getDefaultCompositeId: () => this.viewDescriptorService.getDefaultViewContainer(this.location)!.id,
@@ -223,24 +233,20 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}));
}
private getContextMenuActionsForComposite(compositeId: string): Action[] {
const actions = [];
private getContextMenuActionsForComposite(compositeId: string): IAction[] {
const actions: IAction[] = [];
const viewContainer = this.viewDescriptorService.getViewContainerById(compositeId)!;
const defaultLocation = this.viewDescriptorService.getDefaultViewContainerLocation(viewContainer)!;
if (defaultLocation !== this.viewDescriptorService.getViewContainerLocation(viewContainer)) {
actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => {
this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation);
}));
actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewContainerToLocation(viewContainer, defaultLocation) }));
} else {
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
if (viewContainerModel.allViewDescriptors.length === 1) {
const viewToReset = viewContainerModel.allViewDescriptors[0];
const defaultContainer = this.viewDescriptorService.getDefaultContainerById(viewToReset.id)!;
if (defaultContainer !== viewContainer) {
actions.push(new Action('resetLocationAction', nls.localize('resetLocation', "Reset Location"), undefined, true, async () => {
this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer);
}));
actions.push(toAction({ id: 'resetLocationAction', label: localize('resetLocation', "Reset Location"), run: () => this.viewDescriptorService.moveViewsToContainer([viewToReset], defaultContainer) }));
}
}
}
@@ -278,7 +284,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}));
}
private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation }>) {
private onDidChangeViewContainers(added: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>, removed: ReadonlyArray<{ container: ViewContainer, location: ViewContainerLocation; }>) {
removed.filter(({ location }) => location === ViewContainerLocation.Sidebar).forEach(({ container }) => this.onDidDeregisterViewContainer(container));
this.onDidRegisterViewContainers(added.filter(({ location }) => location === ViewContainerLocation.Sidebar).map(({ container }) => container));
}
@@ -310,7 +316,22 @@ export class ActivitybarPart extends Part implements IActivityBarService {
}
private onDidRegisterExtensions(): void {
this.removeNotExistingComposites();
this.hasExtensionsRegistered = true;
// show/hide/remove composites
for (const { id } of this.cachedViewContainers) {
const viewContainer = this.getViewContainer(id);
if (viewContainer) {
this.showOrHideViewContainer(viewContainer);
} else {
if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) {
this.removeComposite(id);
} else {
this.hideComposite(id);
}
}
}
this.saveCachedViewContainers();
}
@@ -322,7 +343,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.compositeBar.addComposite(viewContainer);
this.compositeBar.activateComposite(viewContainer.id);
if (viewContainer.hideIfEmpty) {
if (this.shouldBeHidden(viewContainer)) {
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
if (viewContainerModel.activeViewDescriptors.length === 0) {
// Update the composite bar by hiding
@@ -460,7 +481,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
// Home action bar
const homeIndicator = this.environmentService.options?.homeIndicator;
if (homeIndicator) {
// TODO @sbatten remove the fake setting and associated code
if (homeIndicator && this.configurationService.getValue<boolean>('window.showHomeIndicator')) {
let codicon = iconRegistry.get(homeIndicator.icon);
if (!codicon) {
codicon = Codicon.code;
@@ -556,14 +578,14 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private createHomeBar(href: string, icon: Codicon): void {
this.homeBarContainer = document.createElement('div');
this.homeBarContainer.setAttribute('aria-label', nls.localize('homeIndicator', "Home"));
this.homeBarContainer.setAttribute('aria-label', localize('homeIndicator', "Home"));
this.homeBarContainer.setAttribute('role', 'toolbar');
this.homeBarContainer.classList.add('home-bar');
this.homeBar = this._register(new ActionBar(this.homeBarContainer, {
actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme)),
actionViewItemProvider: action => this.instantiationService.createInstance(HomeActivityActionViewItem, href, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme)),
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('home', "Home"),
ariaLabel: localize('home', "Home"),
animated: false,
preventLoopNavigation: true,
ignoreOrientationForPreviousAndNextKey: true
@@ -575,7 +597,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.homeBar.push(this._register(new ActivityAction({
id: 'workbench.actions.home',
name: nls.localize('home', "Home"),
name: localize('home', "Home"),
cssClass: icon.classNames
})));
@@ -587,17 +609,17 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.globalActivityActionBar = this._register(new ActionBar(container, {
actionViewItemProvider: action => {
if (action.id === 'workbench.actions.manage') {
return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme));
return this.instantiationService.createInstance(GlobalActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme));
}
if (action.id === 'workbench.actions.accounts') {
return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, (theme: IColorTheme) => this.getActivitybarItemColors(theme));
return this.instantiationService.createInstance(AccountsActivityActionViewItem, action as ActivityAction, () => this.compositeBar.getContextMenuActions(), (theme: IColorTheme) => this.getActivitybarItemColors(theme));
}
throw new Error(`No view item for action '${action.id}'`);
},
orientation: ActionsOrientation.VERTICAL,
ariaLabel: nls.localize('manage', "Manage"),
ariaLabel: localize('manage', "Manage"),
animated: false,
preventLoopNavigation: true,
ignoreOrientationForPreviousAndNextKey: true
@@ -605,15 +627,15 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.globalActivityAction = this._register(new ActivityAction({
id: 'workbench.actions.manage',
name: nls.localize('manage', "Manage"),
cssClass: ThemeIcon.asClassName(settingsViewBarIcon)
name: localize('manage', "Manage"),
cssClass: ThemeIcon.asClassName(ActivitybarPart.GEAR_ICON)
}));
if (this.accountsVisibilityPreference) {
this.accountsActivityAction = this._register(new ActivityAction({
id: 'workbench.actions.accounts',
name: nls.localize('accounts', "Accounts"),
cssClass: ThemeIcon.asClassName(accountsViewBarIcon)
name: localize('accounts', "Accounts"),
cssClass: ThemeIcon.asClassName(ActivitybarPart.ACCOUNTS_ICON)
}));
this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX });
@@ -630,7 +652,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
} else {
this.accountsActivityAction = this._register(new ActivityAction({
id: 'workbench.actions.accounts',
name: nls.localize('accounts', "Accounts"),
name: localize('accounts', "Accounts"),
cssClass: Codicon.account.classNames
}));
this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX });
@@ -640,7 +662,7 @@ export class ActivitybarPart extends Part implements IActivityBarService {
this.updateGlobalActivity(ACCOUNTS_ACTIVITY_ID);
}
private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } {
private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction; } {
let compositeActions = this.compositeActions.get(compositeId);
if (!compositeActions) {
const viewContainer = this.getViewContainer(compositeId);
@@ -666,32 +688,27 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private onDidRegisterViewContainers(viewContainers: ReadonlyArray<ViewContainer>): void {
for (const viewContainer of viewContainers) {
this.compositeBar.addComposite(viewContainer);
// Pin it by default if it is new
const cachedViewContainer = this.cachedViewContainers.filter(({ id }) => id === viewContainer.id)[0];
const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location);
const isActive = visibleViewContainer?.id === viewContainer.id;
if (isActive || !this.shouldBeHidden(viewContainer.id, cachedViewContainer)) {
this.compositeBar.addComposite(viewContainer);
// Pin it by default if it is new
if (!cachedViewContainer) {
this.compositeBar.pin(viewContainer.id);
}
if (isActive) {
this.compositeBar.activateComposite(viewContainer.id);
}
if (!cachedViewContainer) {
this.compositeBar.pin(viewContainer.id);
}
// Active
const visibleViewContainer = this.viewsService.getVisibleViewContainer(this.location);
if (visibleViewContainer?.id === viewContainer.id) {
this.compositeBar.activateComposite(viewContainer.id);
}
}
for (const viewContainer of viewContainers) {
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
this.updateActivity(viewContainer, viewContainerModel);
this.onDidChangeActiveViews(viewContainer, viewContainerModel);
this.showOrHideViewContainer(viewContainer);
const disposables = new DisposableStore();
disposables.add(viewContainerModel.onDidChangeContainerInfo(() => this.updateActivity(viewContainer, viewContainerModel)));
disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.onDidChangeActiveViews(viewContainer, viewContainerModel)));
disposables.add(viewContainerModel.onDidChangeActiveViewDescriptors(() => this.showOrHideViewContainer(viewContainer)));
this.viewContainerDisposables.set(viewContainer.id, disposables);
}
@@ -728,12 +745,15 @@ export class ActivitybarPart extends Part implements IActivityBarService {
let iconUrl: URI | undefined = undefined;
if (URI.isUri(icon)) {
iconUrl = icon;
cssClass = `activity-${id.replace(/\./g, '-')}`;
const cssUrl = asCSSUrl(icon);
const hash = new StringSHA1();
hash.update(cssUrl);
cssClass = `activity-${id.replace(/\./g, '-')}-${hash.digest()}`;
const iconClass = `.monaco-workbench .activitybar .monaco-action-bar .action-label.${cssClass}`;
createCSSRule(iconClass, `
mask: ${asCSSUrl(icon)} no-repeat 50% 50%;
mask: ${cssUrl} no-repeat 50% 50%;
mask-size: 24px;
-webkit-mask: ${asCSSUrl(icon)} no-repeat 50% 50%;
-webkit-mask: ${cssUrl} no-repeat 50% 50%;
-webkit-mask-size: 24px;
`);
} else if (ThemeIcon.isThemeIcon(icon)) {
@@ -743,36 +763,43 @@ export class ActivitybarPart extends Part implements IActivityBarService {
return { id, name, cssClass, iconUrl, keybindingId };
}
private onDidChangeActiveViews(viewContainer: ViewContainer, viewContainerModel: IViewContainerModel): void {
if (viewContainerModel.activeViewDescriptors.length) {
this.compositeBar.addComposite(viewContainer);
} else if (viewContainer.hideIfEmpty) {
private showOrHideViewContainer(viewContainer: ViewContainer): void {
if (this.shouldBeHidden(viewContainer)) {
this.hideComposite(viewContainer.id);
} else {
this.compositeBar.addComposite(viewContainer);
}
}
private shouldBeHidden(viewContainerId: string, cachedViewContainer?: ICachedViewContainer): boolean {
const viewContainer = this.getViewContainer(viewContainerId);
if (!viewContainer || !viewContainer.hideIfEmpty) {
return false;
}
private shouldBeHidden(viewContainerOrId: string | ViewContainer, cachedViewContainer?: ICachedViewContainer): boolean {
const viewContainer = isString(viewContainerOrId) ? this.getViewContainer(viewContainerOrId) : viewContainerOrId;
const viewContainerId = isString(viewContainerOrId) ? viewContainerOrId : viewContainerOrId.id;
return cachedViewContainer?.views && cachedViewContainer.views.length
? cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when)))
: viewContainerId === TEST_VIEW_CONTAINER_ID /* Hide Test view container for the first time or it had no views registered before */;
}
private removeNotExistingComposites(): void {
const viewContainers = this.getViewContainers();
for (const { id } of this.cachedViewContainers) {
if (viewContainers.every(viewContainer => viewContainer.id !== id)) {
if (this.viewDescriptorService.isViewContainerRemovedPermanently(id)) {
this.removeComposite(id);
} else {
this.hideComposite(id);
if (viewContainer) {
if (viewContainer.hideIfEmpty) {
if (this.viewDescriptorService.getViewContainerModel(viewContainer).activeViewDescriptors.length > 0) {
return false;
}
} else {
return false;
}
}
// Check cache only if extensions are not yet registered and current window is not native (desktop) remote connection window
if (!this.hasExtensionsRegistered && !(this.environmentService.remoteAuthority && isNative)) {
cachedViewContainer = cachedViewContainer || this.cachedViewContainers.find(({ id }) => id === viewContainerId);
// Show builtin ViewContainer if not registered yet
if (!viewContainer && cachedViewContainer?.isBuiltin) {
return false;
}
if (cachedViewContainer?.views?.length) {
return cachedViewContainer.views.every(({ when }) => !!when && !this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(when)));
}
}
return true;
}
private hideComposite(compositeId: string): void {
@@ -864,7 +891,6 @@ export class ActivitybarPart extends Part implements IActivityBarService {
private getViewContainer(id: string): ViewContainer | undefined {
const viewContainer = this.viewDescriptorService.getViewContainerById(id);
return viewContainer && this.viewDescriptorService.getViewContainerLocation(viewContainer) === this.location ? viewContainer : undefined;
}
@@ -918,22 +944,22 @@ export class ActivitybarPart extends Part implements IActivityBarService {
const viewContainer = this.getViewContainer(compositeItem.id);
if (viewContainer) {
const viewContainerModel = this.viewDescriptorService.getViewContainerModel(viewContainer);
const views: { when: string | undefined }[] = [];
const views: { when: string | undefined; }[] = [];
for (const { when } of viewContainerModel.allViewDescriptors) {
views.push({ when: when ? when.serialize() : undefined });
}
const cacheIcon = URI.isUri(viewContainerModel.icon) ? viewContainerModel.icon.scheme === Schemas.file : true;
state.push({
id: compositeItem.id,
name: viewContainerModel.title,
icon: cacheIcon ? viewContainerModel.icon : undefined,
icon: URI.isUri(viewContainerModel.icon) && this.environmentService.remoteAuthority && isNative ? undefined : viewContainerModel.icon, /* Donot cache uri icons in desktop with remote connection */
views,
pinned: compositeItem.pinned,
order: compositeItem.order,
visible: compositeItem.visible
visible: compositeItem.visible,
isBuiltin: !viewContainer.extensionId
});
} else {
state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false });
state.push({ id: compositeItem.id, pinned: compositeItem.pinned, order: compositeItem.order, visible: false, isBuiltin: false });
}
}
@@ -948,9 +974,10 @@ export class ActivitybarPart extends Part implements IActivityBarService {
const cachedViewContainer = this._cachedViewContainers.filter(cached => cached.id === placeholderViewContainer.id)[0];
if (cachedViewContainer) {
cachedViewContainer.name = placeholderViewContainer.name;
cachedViewContainer.icon = placeholderViewContainer.themeIcon ? ThemeIcon.revive(placeholderViewContainer.themeIcon) :
cachedViewContainer.icon = placeholderViewContainer.themeIcon ? placeholderViewContainer.themeIcon :
placeholderViewContainer.iconUrl ? URI.revive(placeholderViewContainer.iconUrl) : undefined;
cachedViewContainer.views = placeholderViewContainer.views;
cachedViewContainer.isBuiltin = placeholderViewContainer.isBuiltin;
}
}
}
@@ -966,11 +993,12 @@ export class ActivitybarPart extends Part implements IActivityBarService {
order
})));
this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views }) => (<IPlaceholderViewContainer>{
this.setPlaceholderViewContainers(cachedViewContainers.map(({ id, icon, name, views, isBuiltin }) => (<IPlaceholderViewContainer>{
id,
iconUrl: URI.isUri(icon) ? icon : undefined,
themeIcon: ThemeIcon.isThemeIcon(icon) ? icon : undefined,
name,
isBuiltin,
views
})));
}
@@ -1067,7 +1095,7 @@ class FocusActivityBarAction extends Action2 {
constructor() {
super({
id: 'workbench.action.focusActivityBar',
title: { value: nls.localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' },
title: { value: localize('focusActivityBar', "Focus Activity Bar"), original: 'Focus Activity Bar' },
category: CATEGORIES.View,
f1: true
});

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import { IAction, toAction } from 'vs/base/common/actions';
import { illegalArgument } from 'vs/base/common/errors';
import * as arrays from 'vs/base/common/arrays';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -149,10 +149,10 @@ export interface ICompositeBarOptions {
readonly preventLoopNavigation?: boolean;
getActivityAction: (compositeId: string) => ActivityAction;
getCompositePinnedAction: (compositeId: string) => Action;
getOnCompositeClickAction: (compositeId: string) => Action;
getContextMenuActions: () => Action[];
getContextMenuActionsForComposite: (compositeId: string) => Action[];
getCompositePinnedAction: (compositeId: string) => IAction;
getOnCompositeClickAction: (compositeId: string) => IAction;
fillExtraContextMenuActions: (actions: IAction[]) => void;
getContextMenuActionsForComposite: (compositeId: string) => IAction[];
openComposite: (compositeId: string) => Promise<IComposite | null>;
getDefaultCompositeId: () => string;
hidePart: () => void;
@@ -208,15 +208,15 @@ export class CompositeBar extends Widget implements ICompositeBar {
create(parent: HTMLElement): HTMLElement {
const actionBarDiv = parent.appendChild($('.composite-bar'));
this.compositeSwitcherBar = this._register(new ActionBar(actionBarDiv, {
actionViewItemProvider: (action: IAction) => {
actionViewItemProvider: action => {
if (action instanceof CompositeOverflowActivityAction) {
return this.compositeOverflowActionViewItem;
}
const item = this.model.findItem(action.id);
return item && this.instantiationService.createInstance(
CompositeActionViewItem, action as ActivityAction, item.pinnedAction,
(compositeId: string) => this.options.getContextMenuActionsForComposite(compositeId),
() => this.getContextMenuActions() as Action[],
compositeId => this.options.getContextMenuActionsForComposite(compositeId),
() => this.getContextMenuActions(),
this.options.colors,
this.options.icon,
this.options.dndHandler,
@@ -319,7 +319,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
this.updateCompositeSwitcher();
}
addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number }): void {
addComposite({ id, name, order, requestedIndex }: { id: string; name: string, order?: number, requestedIndex?: number; }): void {
// Add to the model
if (this.model.add(id, name, order, requestedIndex)) {
this.computeSizes([this.model.findItem(id)]);
@@ -596,7 +596,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
this.compositeOverflowAction,
() => this.getOverflowingComposites(),
() => this.model.activeItem ? this.model.activeItem.id : undefined,
(compositeId: string) => {
compositeId => {
const item = this.model.findItem(compositeId);
return item?.activity[0]?.badge;
},
@@ -610,7 +610,7 @@ export class CompositeBar extends Widget implements ICompositeBar {
this._onDidChange.fire();
}
private getOverflowingComposites(): { id: string, name?: string }[] {
private getOverflowingComposites(): { id: string, name?: string; }[] {
let overflowingIds = this.model.visibleItems.filter(item => item.pinned).map(item => item.id);
// Show the active composite even if it is not pinned
@@ -631,9 +631,9 @@ export class CompositeBar extends Widget implements ICompositeBar {
});
}
private getContextMenuActions(): IAction[] {
getContextMenuActions(): IAction[] {
const actions: IAction[] = this.model.visibleItems
.map(({ id, name, activityAction }) => (<IAction>{
.map(({ id, name, activityAction }) => (toAction({
id,
label: this.getAction(id).label || name || id,
checked: this.isPinned(id),
@@ -645,19 +645,17 @@ export class CompositeBar extends Widget implements ICompositeBar {
this.pin(id, true);
}
}
}));
const otherActions = this.options.getContextMenuActions();
if (otherActions.length) {
actions.push(new Separator());
actions.push(...otherActions);
}
})));
this.options.fillExtraContextMenuActions(actions);
return actions;
}
}
interface ICompositeBarModelItem extends ICompositeBarItem {
activityAction: ActivityAction;
pinnedAction: Action;
pinnedAction: IAction;
activity: ICompositeActivity[];
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { Action, Separator } from 'vs/base/common/actions';
import { Action, IAction, Separator } from 'vs/base/common/actions';
import * as dom from 'vs/base/browser/dom';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { dispose, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
@@ -374,14 +374,14 @@ export class CompositeOverflowActivityAction extends ActivityAction {
}
export class CompositeOverflowActivityActionViewItem extends ActivityActionViewItem {
private actions: Action[] = [];
private actions: IAction[] = [];
constructor(
action: ActivityAction,
private getOverflowingComposites: () => { id: string, name?: string }[],
private getActiveCompositeId: () => string | undefined,
private getBadge: (compositeId: string) => IBadge,
private getCompositeOpenAction: (compositeId: string) => Action,
private getCompositeOpenAction: (compositeId: string) => IAction,
colors: (theme: IColorTheme) => ICompositeBarColors,
@IContextMenuService private readonly contextMenuService: IContextMenuService,
@IThemeService themeService: IThemeService
@@ -404,7 +404,7 @@ export class CompositeOverflowActivityActionViewItem extends ActivityActionViewI
});
}
private getActions(): Action[] {
private getActions(): IAction[] {
return this.getOverflowingComposites().map(composite => {
const action = this.getCompositeOpenAction(composite.id);
action.checked = this.getActiveCompositeId() === action.id;
@@ -457,9 +457,9 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
constructor(
private compositeActivityAction: ActivityAction,
private toggleCompositePinnedAction: Action,
private compositeContextMenuActionsProvider: (compositeId: string) => ReadonlyArray<Action>,
private contextMenuActionsProvider: () => ReadonlyArray<Action>,
private toggleCompositePinnedAction: IAction,
private compositeContextMenuActionsProvider: (compositeId: string) => IAction[],
private contextMenuActionsProvider: () => IAction[],
colors: (theme: IColorTheme) => ICompositeBarColors,
icon: boolean,
private dndHandler: ICompositeDragAndDrop,
@@ -500,9 +500,14 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
return this.compositeActivity;
}
private getActivtyName(): string {
private getActivtyName(skipKeybinding = false): string {
let name = this.compositeActivityAction.activity.name;
if (skipKeybinding) {
return name;
}
const keybinding = this.compositeActivityAction.activity.keybindingId ? this.keybindingService.lookupKeybinding(this.compositeActivityAction.activity.keybindingId) : null;
return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", this.compositeActivityAction.activity.name, keybinding.getLabel()) : this.compositeActivityAction.activity.name;
return keybinding ? nls.localize('titleKeybinding', "{0} ({1})", name, keybinding.getLabel()) : name;
}
render(container: HTMLElement): void {
@@ -601,7 +606,7 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
}
private showContextMenu(container: HTMLElement): void {
const actions: Action[] = [this.toggleCompositePinnedAction];
const actions: IAction[] = [this.toggleCompositePinnedAction];
const compositeContextMenuActions = this.compositeContextMenuActionsProvider(this.activity.id);
if (compositeContextMenuActions.length) {
@@ -615,10 +620,10 @@ export class CompositeActionViewItem extends ActivityActionViewItem {
const isPinned = this.compositeBar.isPinned(this.activity.id);
if (isPinned) {
this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide");
this.toggleCompositePinnedAction.label = nls.localize('hide', "Hide '{0}'", this.getActivtyName(true));
this.toggleCompositePinnedAction.checked = false;
} else {
this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep");
this.toggleCompositePinnedAction.label = nls.localize('keep', "Keep '{0}'", this.getActivtyName(true));
}
const otherActions = this.contextMenuActionsProvider();

View File

@@ -327,10 +327,6 @@ export abstract class CompositePart<T extends Composite> extends Part {
const primaryActions: IAction[] = composite?.getActions().slice(0) || [];
const secondaryActions: IAction[] = composite?.getSecondaryActions().slice(0) || [];
// From Part
primaryActions.push(...this.getActions());
secondaryActions.push(...this.getSecondaryActions());
// Update context
const toolBar = assertIsDefined(this.toolBar);
toolBar.context = this.actionsContextProvider();
@@ -471,14 +467,6 @@ export abstract class CompositePart<T extends Composite> extends Part {
return compositeItem ? compositeItem.progress : undefined;
}
protected getActions(): ReadonlyArray<IAction> {
return [];
}
protected getSecondaryActions(): ReadonlyArray<IAction> {
return [];
}
protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
return AnchorAlignment.RIGHT;
}

View File

@@ -19,9 +19,9 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
import { Disposable } from 'vs/base/common/lifecycle';
export class DialogHandlerContribution extends Disposable implements IWorkbenchContribution {
private impl: IDialogHandler;
private readonly model: IDialogsModel;
private readonly impl: IDialogHandler;
private model: IDialogsModel;
private currentDialog: IDialogViewItem | undefined;
constructor(

View File

@@ -6,19 +6,13 @@
import * as dom from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { BreadcrumbsItem, BreadcrumbsWidget, IBreadcrumbsItemEvent } from 'vs/base/browser/ui/breadcrumbs/breadcrumbsWidget';
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { tail } from 'vs/base/common/arrays';
import { timeout } from 'vs/base/common/async';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { combinedDisposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
import { extUri } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/breadcrumbscontrol';
import { ICodeEditor, isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { Range } from 'vs/editor/common/core/range';
import { ICodeEditorViewState, ScrollType } from 'vs/editor/common/editorCommon';
import { SymbolKinds } from 'vs/editor/common/modes';
import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { localize } from 'vs/nls';
import { MenuId, MenuRegistry } from 'vs/platform/actions/common/actions';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
@@ -28,35 +22,92 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
import { IListService, WorkbenchDataTree, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { ColorIdentifier, ColorFunction } from 'vs/platform/theme/common/colorRegistry';
import { attachBreadcrumbsStyler } from 'vs/platform/theme/common/styler';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { ResourceLabel } from 'vs/workbench/browser/labels';
import { BreadcrumbsConfig, IBreadcrumbsService } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbElement, EditorBreadcrumbsModel, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { BreadcrumbsPicker, createBreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker';
import { BreadcrumbsModel, FileElement, OutlineElement2 } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { BreadcrumbsFilePicker, BreadcrumbsOutlinePicker, BreadcrumbsPicker } from 'vs/workbench/browser/parts/editor/breadcrumbsPicker';
import { IEditorPartOptions, EditorResourceAccessor, SideBySideEditor } from 'vs/workbench/common/editor';
import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, IEditorService, SIDE_GROUP, SIDE_GROUP_TYPE } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
import { withNullAsUndefined, withUndefinedAsNull } from 'vs/base/common/types';
import { ILabelService } from 'vs/platform/label/common/label';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
import { CATEGORIES } from 'vs/workbench/common/actions';
import { ITreeNode } from 'vs/base/browser/ui/tree/tree';
import { IOutline } from 'vs/workbench/services/outline/browser/outline';
class Item extends BreadcrumbsItem {
class OutlineItem extends BreadcrumbsItem {
private readonly _disposables = new DisposableStore();
constructor(
readonly element: BreadcrumbElement,
readonly model: BreadcrumbsModel,
readonly element: OutlineElement2,
readonly options: IBreadcrumbsControlOptions
) {
super();
}
dispose(): void {
this._disposables.dispose();
}
equals(other: BreadcrumbsItem): boolean {
if (!(other instanceof OutlineItem)) {
return false;
}
return this.element === other.element &&
this.options.showFileIcons === other.options.showFileIcons &&
this.options.showSymbolIcons === other.options.showSymbolIcons;
}
render(container: HTMLElement): void {
const { element, outline } = this.element;
if (element === outline) {
const element = dom.$('span', undefined, '…');
container.appendChild(element);
return;
}
const templateId = outline.config.delegate.getTemplateId(element);
const renderer = outline.config.renderers.find(renderer => renderer.templateId === templateId);
if (!renderer) {
container.innerText = '<<NO RENDERER>>';
return;
}
const template = renderer.renderTemplate(container);
renderer.renderElement(<ITreeNode<any, any>>{
element,
children: [],
depth: 0,
visibleChildrenCount: 0,
visibleChildIndex: 0,
collapsible: false,
collapsed: false,
visible: true,
filterData: undefined
}, 0, template, undefined);
this._disposables.add(toDisposable(() => { renderer.disposeTemplate(template); }));
}
}
class FileItem extends BreadcrumbsItem {
private readonly _disposables = new DisposableStore();
constructor(
readonly model: BreadcrumbsModel,
readonly element: FileElement,
readonly options: IBreadcrumbsControlOptions,
@IInstantiationService private readonly _instantiationService: IInstantiationService
) {
@@ -68,59 +119,26 @@ class Item extends BreadcrumbsItem {
}
equals(other: BreadcrumbsItem): boolean {
if (!(other instanceof Item)) {
if (!(other instanceof FileItem)) {
return false;
}
if (this.element instanceof FileElement && other.element instanceof FileElement) {
return (extUri.isEqual(this.element.uri, other.element.uri) &&
this.options.showFileIcons === other.options.showFileIcons &&
this.options.showSymbolIcons === other.options.showSymbolIcons);
}
if (this.element instanceof TreeElement && other.element instanceof TreeElement) {
return this.element.id === other.element.id;
}
return false;
return (extUri.isEqual(this.element.uri, other.element.uri) &&
this.options.showFileIcons === other.options.showFileIcons &&
this.options.showSymbolIcons === other.options.showSymbolIcons);
}
render(container: HTMLElement): void {
if (this.element instanceof FileElement) {
// file/folder
let label = this._instantiationService.createInstance(ResourceLabel, container, {});
label.element.setFile(this.element.uri, {
hidePath: true,
hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons,
fileKind: this.element.kind,
fileDecorations: { colors: this.options.showDecorationColors, badges: false },
});
container.classList.add(FileKind[this.element.kind].toLowerCase());
this._disposables.add(label);
} else if (this.element instanceof OutlineModel) {
// has outline element but not in one
let label = document.createElement('div');
label.innerText = '\u2026';
label.className = 'hint-more';
container.appendChild(label);
} else if (this.element instanceof OutlineGroup) {
// provider
let label = new IconLabel(container);
label.setLabel(this.element.label);
this._disposables.add(label);
} else if (this.element instanceof OutlineElement) {
// symbol
if (this.options.showSymbolIcons) {
let icon = document.createElement('div');
icon.className = SymbolKinds.toCssClassName(this.element.symbol.kind);
container.appendChild(icon);
container.classList.add('shows-symbol-icon');
}
let label = new IconLabel(container);
let title = this.element.symbol.name.replace(/\r|\n|\r\n/g, '\u23CE');
label.setLabel(title);
this._disposables.add(label);
}
// file/folder
let label = this._instantiationService.createInstance(ResourceLabel, container, {});
label.element.setFile(this.element.uri, {
hidePath: true,
hideIcon: this.element.kind === FileKind.FOLDER || !this.options.showFileIcons,
fileKind: this.element.kind,
fileDecorations: { colors: this.options.showDecorationColors, badges: false },
});
container.classList.add(FileKind[this.element.kind].toLowerCase());
this._disposables.add(label);
}
}
@@ -170,26 +188,23 @@ export class BreadcrumbsControl {
private readonly _editorGroup: IEditorGroupView,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IEditorService private readonly _editorService: IEditorService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IThemeService private readonly _themeService: IThemeService,
@IQuickInputService private readonly _quickInputService: IQuickInputService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService,
@IFileService private readonly _fileService: IFileService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IEditorService private readonly _editorService: IEditorService,
@ILabelService private readonly _labelService: ILabelService,
@IConfigurationService configurationService: IConfigurationService,
@IBreadcrumbsService breadcrumbsService: IBreadcrumbsService,
) {
this.domNode = document.createElement('div');
this.domNode.classList.add('breadcrumbs-control');
dom.append(container, this.domNode);
this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(_configurationService);
this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(_configurationService);
this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(_configurationService);
this._cfUseQuickPick = BreadcrumbsConfig.UseQuickPick.bindTo(configurationService);
this._cfShowIcons = BreadcrumbsConfig.Icons.bindTo(configurationService);
this._cfTitleScrollbarSizing = BreadcrumbsConfig.TitleScrollbarSizing.bindTo(configurationService);
const sizing = this._cfTitleScrollbarSizing.getValue() ?? 'default';
this._widget = new BreadcrumbsWidget(this.domNode, BreadcrumbsControl.SCROLLBAR_SIZES[sizing]);
@@ -256,14 +271,11 @@ export class BreadcrumbsControl {
this._ckBreadcrumbsVisible.set(true);
this._ckBreadcrumbsPossible.set(true);
const editor = this._getActiveCodeEditor();
const model = new EditorBreadcrumbsModel(
const model = this._instantiationService.createInstance(BreadcrumbsModel,
fileInfoUri ?? uri,
uri, editor,
this._configurationService,
this._textResourceConfigurationService,
this._workspaceService
this._editorGroup.activeEditorPane
);
this.domNode.classList.toggle('relative-path', model.isRelative());
this.domNode.classList.toggle('backslash-path', this._labelService.getSeparator(uri.scheme, uri.authority) === '\\');
@@ -274,7 +286,7 @@ export class BreadcrumbsControl {
showFileIcons: this._options.showFileIcons && showIcons,
showSymbolIcons: this._options.showSymbolIcons && showIcons
};
const items = model.getElements().map(element => new Item(element, options, this._instantiationService));
const items = model.getElements().map(element => element instanceof FileElement ? new FileItem(model, element, options, this._instantiationService) : new OutlineItem(model, element, options));
this._widget.setItems(items);
this._widget.reveal(items[items.length - 1]);
};
@@ -298,7 +310,7 @@ export class BreadcrumbsControl {
this._breadcrumbsDisposables.add({
dispose: () => {
if (this._breadcrumbsPickerShowing) {
this._contextViewService.hideContextView(this);
this._contextViewService.hideContextView({ source: this });
}
}
});
@@ -306,20 +318,6 @@ export class BreadcrumbsControl {
return true;
}
private _getActiveCodeEditor(): ICodeEditor | undefined {
if (!this._editorGroup.activeEditorPane) {
return undefined;
}
let control = this._editorGroup.activeEditorPane.getControl();
let editor: ICodeEditor | undefined;
if (isCodeEditor(control)) {
editor = control as ICodeEditor;
} else if (isDiffEditor(control)) {
editor = control.getModifiedEditor();
}
return editor;
}
private _onFocusEvent(event: IBreadcrumbsItemEvent): void {
if (event.item && this._breadcrumbsPickerShowing) {
this._breadcrumbsPickerIgnoreOnceItem = undefined;
@@ -339,13 +337,12 @@ export class BreadcrumbsControl {
return;
}
const { element } = event.item as Item;
const { element } = event.item as FileItem | OutlineItem;
this._editorGroup.focus();
type BreadcrumbSelectClassification = {
type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
};
this._telemetryService.publicLog2<{ type: string }, BreadcrumbSelectClassification>('breadcrumbs/select', { type: element instanceof TreeElement ? 'symbol' : 'file' });
type BreadcrumbSelect = { type: string };
type BreadcrumbSelectClassification = { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' }; };
this._telemetryService.publicLog2<BreadcrumbSelect, BreadcrumbSelectClassification>('breadcrumbs/select', { type: event.item instanceof OutlineItem ? 'symbol' : 'file' });
const group = this._getEditorGroup(event.payload);
if (group !== undefined) {
@@ -360,64 +357,31 @@ export class BreadcrumbsControl {
// using quick pick
this._widget.setFocused(undefined);
this._widget.setSelection(undefined);
this._quickInputService.quickAccess.show(element instanceof TreeElement ? '@' : '');
this._quickInputService.quickAccess.show(element instanceof OutlineElement2 ? '@' : '');
return;
}
// show picker
let picker: BreadcrumbsPicker;
let pickerAnchor: { x: number; y: number };
let editor = this._getActiveCodeEditor();
let editorDecorations: string[] = [];
let editorViewState: ICodeEditorViewState | undefined;
interface IHideData { didPick?: boolean, source?: BreadcrumbsControl }
this._contextViewService.showContextView({
render: (parent: HTMLElement) => {
picker = createBreadcrumbsPicker(this._instantiationService, parent, element);
let selectListener = picker.onDidPickElement(data => {
if (data.target) {
editorViewState = undefined;
}
this._contextViewService.hideContextView(this);
if (event.item instanceof FileItem) {
picker = this._instantiationService.createInstance(BreadcrumbsFilePicker, parent, event.item.model.resource);
} else if (event.item instanceof OutlineItem) {
picker = this._instantiationService.createInstance(BreadcrumbsOutlinePicker, parent, event.item.model.resource);
}
const group = (picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).metaKey) || (!picker.useAltAsMultipleSelectionModifier && (data.browserEvent as MouseEvent).altKey)
? SIDE_GROUP
: ACTIVE_GROUP;
this._revealInEditor(event, data.target, group, (data.browserEvent as MouseEvent).button === 1);
/* __GDPR__
"breadcrumbs/open" : {
"type": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
this._telemetryService.publicLog('breadcrumbs/open', { type: !data ? 'nothing' : data.target instanceof TreeElement ? 'symbol' : 'file' });
});
let focusListener = picker.onDidFocusElement(data => {
if (!editor || !(data.target instanceof OutlineElement)) {
return;
}
if (!editorViewState) {
editorViewState = withNullAsUndefined(editor.saveViewState());
}
const { symbol } = data.target;
editor.revealRangeInCenterIfOutsideViewport(symbol.range, ScrollType.Smooth);
editorDecorations = editor.deltaDecorations(editorDecorations, [{
range: symbol.range,
options: {
className: 'rangeHighlight',
isWholeLine: true
}
}]);
});
let zoomListener = onDidChangeZoomLevel(() => {
this._contextViewService.hideContextView(this);
});
let selectListener = picker.onWillPickElement(() => this._contextViewService.hideContextView({ source: this, didPick: true }));
let zoomListener = onDidChangeZoomLevel(() => this._contextViewService.hideContextView({ source: this }));
let focusTracker = dom.trackFocus(parent);
let blurListener = focusTracker.onDidBlur(() => {
this._breadcrumbsPickerIgnoreOnceItem = this._widget.isDOMFocused() ? event.item : undefined;
this._contextViewService.hideContextView(this);
this._contextViewService.hideContextView({ source: this });
});
this._breadcrumbsPickerShowing = true;
@@ -426,7 +390,6 @@ export class BreadcrumbsControl {
return combinedDisposable(
picker,
selectListener,
focusListener,
zoomListener,
focusTracker,
blurListener
@@ -465,19 +428,17 @@ export class BreadcrumbsControl {
}
return pickerAnchor;
},
onHide: (data) => {
if (editor) {
editor.deltaDecorations(editorDecorations, []);
if (editorViewState) {
editor.restoreViewState(editorViewState);
}
onHide: (data?: IHideData) => {
if (!data?.didPick) {
picker.restoreViewState();
}
this._breadcrumbsPickerShowing = false;
this._updateCkBreadcrumbsActive();
if (data === this) {
if (data?.source === this) {
this._widget.setFocused(undefined);
this._widget.setSelection(undefined);
}
picker.dispose();
}
});
}
@@ -487,11 +448,11 @@ export class BreadcrumbsControl {
this._ckBreadcrumbsActive.set(value);
}
private _revealInEditor(event: IBreadcrumbsItemEvent, element: BreadcrumbElement, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): void {
private async _revealInEditor(event: IBreadcrumbsItemEvent, element: FileElement | OutlineElement2, group: SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | undefined, pinned: boolean = false): Promise<void> {
if (element instanceof FileElement) {
if (element.kind === FileKind.FILE) {
// open file in any editor
this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group);
await this._editorService.openEditor({ resource: element.uri, options: { pinned } }, group);
} else {
// show next picker
let items = this._widget.getItems();
@@ -499,20 +460,8 @@ export class BreadcrumbsControl {
this._widget.setFocused(items[idx + 1]);
this._widget.setSelection(items[idx + 1], BreadcrumbsControl.Payload_Pick);
}
} else if (element instanceof OutlineElement) {
// open symbol in code editor
const model = OutlineModel.get(element);
if (model) {
this._codeEditorService.openCodeEditor({
resource: model.uri,
options: {
selection: Range.collapseToStart(element.symbol.selectionRange),
selectionRevealType: TextEditorSelectionRevealType.CenterIfOutsideViewport,
pinned
}
}, withUndefinedAsNull(this._getActiveCodeEditor()), group === SIDE_GROUP);
}
} else {
element.outline.reveal(element, { pinned }, group === SIDE_GROUP);
}
}
@@ -744,29 +693,29 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
handler(accessor) {
const editors = accessor.get(IEditorService);
const lists = accessor.get(IListService);
const element = lists.lastFocusedList ? <OutlineElement | IFileStat>lists.lastFocusedList.getFocus()[0] : undefined;
if (element instanceof OutlineElement) {
const outlineElement = OutlineModel.get(element);
if (!outlineElement) {
return undefined;
}
// open symbol in editor
return editors.openEditor({
resource: outlineElement.uri,
options: { selection: Range.collapseToStart(element.symbol.selectionRange), pinned: true }
}, SIDE_GROUP);
const tree = lists.lastFocusedList;
if (!(tree instanceof WorkbenchDataTree)) {
return;
}
} else if (element && URI.isUri(element.resource)) {
// open file in editor
const element = <IFileStat | unknown>tree.getFocus()[0];
if (URI.isUri((<IFileStat>element)?.resource)) {
// IFileStat: open file in editor
return editors.openEditor({
resource: element.resource,
resource: (<IFileStat>element).resource,
options: { pinned: true }
}, SIDE_GROUP);
}
} else {
// ignore
return undefined;
// IOutline: check if this the outline and iff so reveal element
const input = tree.getInput();
if (input && typeof (<IOutline<any>>input).outlineKind === 'string') {
return (<IOutline<any>>input).reveal(element, {
pinned: true,
preserveFocus: false
}, true);
}
}
});

View File

@@ -3,27 +3,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { equals } from 'vs/base/common/arrays';
import { TimeoutTimer } from 'vs/base/common/async';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { DisposableStore, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { isEqual, dirname } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IPosition } from 'vs/editor/common/core/position';
import { DocumentSymbolProviderRegistry } from 'vs/editor/common/modes';
import { OutlineElement, OutlineGroup, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { IWorkspaceContextService, IWorkspaceFolder, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { Schemas } from 'vs/base/common/network';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { FileKind } from 'vs/platform/files/common/files';
import { withNullAsUndefined } from 'vs/base/common/types';
import { OutlineFilter } from 'vs/editor/contrib/documentSymbols/outlineTree';
import { ITextModel } from 'vs/editor/common/model';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
import { IOutline, IOutlineService, OutlineTarget } from 'vs/workbench/services/outline/browser/outline';
import { IEditorPane } from 'vs/workbench/common/editor';
export class FileElement {
constructor(
@@ -32,11 +25,16 @@ export class FileElement {
) { }
}
export type BreadcrumbElement = FileElement | OutlineModel | OutlineGroup | OutlineElement;
type FileInfo = { path: FileElement[], folder?: IWorkspaceFolder };
export class EditorBreadcrumbsModel {
export class OutlineElement2 {
constructor(
readonly element: IOutline<any> | any,
readonly outline: IOutline<any>
) { }
}
export class BreadcrumbsModel {
private readonly _disposables = new DisposableStore();
private readonly _fileInfo: FileInfo;
@@ -45,28 +43,31 @@ export class EditorBreadcrumbsModel {
private readonly _cfgFilePath: BreadcrumbsConfig<'on' | 'off' | 'last'>;
private readonly _cfgSymbolPath: BreadcrumbsConfig<'on' | 'off' | 'last'>;
private _outlineElements: Array<OutlineModel | OutlineGroup | OutlineElement> = [];
private _outlineDisposables = new DisposableStore();
private readonly _currentOutline = new MutableDisposable<IOutline<any>>();
private readonly _outlineDisposables = new DisposableStore();
private readonly _onDidUpdate = new Emitter<this>();
readonly onDidUpdate: Event<this> = this._onDidUpdate.event;
constructor(
fileInfoUri: URI,
private readonly _uri: URI,
private readonly _editor: ICodeEditor | undefined,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITextResourceConfigurationService private readonly _textResourceConfigurationService: ITextResourceConfigurationService,
@IWorkspaceContextService workspaceService: IWorkspaceContextService,
readonly resource: URI,
editor: IEditorPane | undefined,
@IConfigurationService configurationService: IConfigurationService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IOutlineService private readonly _outlineService: IOutlineService,
) {
this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(_configurationService);
this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(_configurationService);
this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(_configurationService);
this._cfgEnabled = BreadcrumbsConfig.IsEnabled.bindTo(configurationService);
this._cfgFilePath = BreadcrumbsConfig.FilePath.bindTo(configurationService);
this._cfgSymbolPath = BreadcrumbsConfig.SymbolPath.bindTo(configurationService);
this._disposables.add(this._cfgFilePath.onDidChange(_ => this._onDidUpdate.fire(this)));
this._disposables.add(this._cfgSymbolPath.onDidChange(_ => this._onDidUpdate.fire(this)));
this._fileInfo = EditorBreadcrumbsModel._initFilePathInfo(fileInfoUri, workspaceService);
this._bindToEditor();
this._fileInfo = this._initFilePathInfo(resource);
if (editor) {
this._bindToEditor(editor);
this._disposables.add(_outlineService.onDidChange(() => this._bindToEditor(editor)));
}
this._onDidUpdate.fire(this);
}
@@ -74,6 +75,7 @@ export class EditorBreadcrumbsModel {
this._cfgEnabled.dispose();
this._cfgFilePath.dispose();
this._cfgSymbolPath.dispose();
this._currentOutline.dispose();
this._outlineDisposables.dispose();
this._disposables.dispose();
this._onDidUpdate.dispose();
@@ -83,8 +85,8 @@ export class EditorBreadcrumbsModel {
return Boolean(this._fileInfo.folder);
}
getElements(): ReadonlyArray<BreadcrumbElement> {
let result: BreadcrumbElement[] = [];
getElements(): ReadonlyArray<FileElement | OutlineElement2> {
let result: (FileElement | OutlineElement2)[] = [];
// file path elements
if (this._cfgFilePath.getValue() === 'on') {
@@ -93,17 +95,27 @@ export class EditorBreadcrumbsModel {
result = result.concat(this._fileInfo.path.slice(-1));
}
// symbol path elements
if (this._cfgSymbolPath.getValue() === 'on') {
result = result.concat(this._outlineElements);
} else if (this._cfgSymbolPath.getValue() === 'last' && this._outlineElements.length > 0) {
result = result.concat(this._outlineElements.slice(-1));
if (this._cfgSymbolPath.getValue() === 'off') {
return result;
}
if (!this._currentOutline.value) {
return result;
}
const breadcrumbsElements = this._currentOutline.value.config.breadcrumbsDataSource.getBreadcrumbElements();
for (let i = this._cfgSymbolPath.getValue() === 'last' && breadcrumbsElements.length > 0 ? breadcrumbsElements.length - 1 : 0; i < breadcrumbsElements.length; i++) {
result.push(new OutlineElement2(breadcrumbsElements[i], this._currentOutline.value));
}
if (breadcrumbsElements.length === 0 && !this._currentOutline.value.isEmpty) {
result.push(new OutlineElement2(this._currentOutline.value, this._currentOutline.value));
}
return result;
}
private static _initFilePathInfo(uri: URI, workspaceService: IWorkspaceContextService): FileInfo {
private _initFilePathInfo(uri: URI): FileInfo {
if (uri.scheme === Schemas.untitled) {
return {
@@ -113,7 +125,7 @@ export class EditorBreadcrumbsModel {
}
let info: FileInfo = {
folder: withNullAsUndefined(workspaceService.getWorkspaceFolder(uri)),
folder: withNullAsUndefined(this._workspaceService.getWorkspaceFolder(uri)),
path: []
};
@@ -130,181 +142,33 @@ export class EditorBreadcrumbsModel {
}
}
if (info.folder && workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
if (info.folder && this._workspaceService.getWorkbenchState() === WorkbenchState.WORKSPACE) {
info.path.unshift(new FileElement(info.folder.uri, FileKind.ROOT_FOLDER));
}
return info;
}
private _bindToEditor(): void {
if (!this._editor) {
return;
}
// update as language, model, providers changes
this._disposables.add(DocumentSymbolProviderRegistry.onDidChange(_ => this._updateOutline()));
this._disposables.add(this._editor.onDidChangeModel(_ => this._updateOutline()));
this._disposables.add(this._editor.onDidChangeModelLanguage(_ => this._updateOutline()));
// update when config changes (re-render)
this._disposables.add(this._configurationService.onDidChangeConfiguration(e => {
if (!this._cfgEnabled.getValue()) {
// breadcrumbs might be disabled (also via a setting/config) and that is
// something we must check before proceeding.
return;
}
if (e.affectsConfiguration('breadcrumbs')) {
this._updateOutline(true);
return;
}
if (this._editor && this._editor.getModel()) {
const editorModel = this._editor.getModel() as ITextModel;
const languageName = editorModel.getLanguageIdentifier().language;
// Checking for changes in the current language override config.
// We can't be more specific than this because the ConfigurationChangeEvent(e) only includes the first part of the root path
if (e.affectsConfiguration(`[${languageName}]`)) {
this._updateOutline(true);
}
}
}));
// update soon'ish as model content change
const updateSoon = new TimeoutTimer();
this._disposables.add(updateSoon);
this._disposables.add(this._editor.onDidChangeModelContent(_ => {
const timeout = OutlineModel.getRequestDelay(this._editor!.getModel());
updateSoon.cancelAndSet(() => this._updateOutline(true), timeout);
}));
this._updateOutline();
// stop when editor dies
this._disposables.add(this._editor.onDidDispose(() => this._outlineDisposables.clear()));
}
private _updateOutline(didChangeContent?: boolean): void {
private _bindToEditor(editor: IEditorPane): void {
const newCts = new CancellationTokenSource();
this._currentOutline.clear();
this._outlineDisposables.clear();
if (!didChangeContent) {
this._updateOutlineElements([]);
}
this._outlineDisposables.add(toDisposable(() => newCts.dispose(true)));
const editor = this._editor!;
const buffer = editor.getModel();
if (!buffer || !DocumentSymbolProviderRegistry.has(buffer) || !isEqual(buffer.uri, this._uri)) {
return;
}
const source = new CancellationTokenSource();
const versionIdThen = buffer.getVersionId();
const timeout = new TimeoutTimer();
this._outlineDisposables.add({
dispose: () => {
source.dispose(true);
timeout.dispose();
this._outlineService.createOutline(editor, OutlineTarget.Breadcrumbs, newCts.token).then(outline => {
if (newCts.token.isCancellationRequested) {
// cancelled: dispose new outline and reset
outline?.dispose();
outline = undefined;
}
});
OutlineModel.create(buffer, source.token).then(model => {
if (source.token.isCancellationRequested) {
// cancelled -> do nothing
return;
this._currentOutline.value = outline;
this._onDidUpdate.fire(this);
if (outline) {
this._outlineDisposables.add(outline.onDidChange(() => this._onDidUpdate.fire(this)));
}
if (TreeElement.empty(model)) {
// empty -> no outline elements
this._updateOutlineElements([]);
} else {
// copy the model
model = model.adopt();
this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition()));
this._outlineDisposables.add(editor.onDidChangeCursorPosition(_ => {
timeout.cancelAndSet(() => {
if (!buffer.isDisposed() && versionIdThen === buffer.getVersionId() && editor.getModel()) {
this._updateOutlineElements(this._getOutlineElements(model, editor.getPosition()));
}
}, 150);
}));
}
}).catch(err => {
this._updateOutlineElements([]);
this._onDidUpdate.fire(this);
onUnexpectedError(err);
});
}
private _getOutlineElements(model: OutlineModel, position: IPosition | null): Array<OutlineModel | OutlineGroup | OutlineElement> {
if (!model || !position) {
return [];
}
let item: OutlineGroup | OutlineElement | undefined = model.getItemEnclosingPosition(position);
if (!item) {
return this._getOutlineElementsRoot(model);
}
let chain: Array<OutlineGroup | OutlineElement> = [];
while (item) {
chain.push(item);
let parent: any = item.parent;
if (parent instanceof OutlineModel) {
break;
}
if (parent instanceof OutlineGroup && parent.parent && parent.parent.children.size === 1) {
break;
}
item = parent;
}
let result: Array<OutlineGroup | OutlineElement> = [];
for (let i = chain.length - 1; i >= 0; i--) {
let element = chain[i];
if (this._isFiltered(element)) {
break;
}
result.push(element);
}
if (result.length === 0) {
return this._getOutlineElementsRoot(model);
}
return result;
}
private _getOutlineElementsRoot(model: OutlineModel): (OutlineModel | OutlineGroup | OutlineElement)[] {
for (const child of model.children.values()) {
if (!this._isFiltered(child)) {
return [model];
}
}
return [];
}
private _isFiltered(element: TreeElement): boolean {
if (element instanceof OutlineElement) {
const key = `breadcrumbs.${OutlineFilter.kindToConfigName[element.symbol.kind]}`;
let uri: URI | undefined;
if (this._editor && this._editor.getModel()) {
const model = this._editor.getModel() as ITextModel;
uri = model.uri;
}
return !this._textResourceConfigurationService.getValue<boolean>(uri, key);
}
return false;
}
private _updateOutlineElements(elements: Array<OutlineModel | OutlineGroup | OutlineElement>): void {
if (!equals(elements, this._outlineElements, EditorBreadcrumbsModel._outlineElementEquals)) {
this._outlineElements = elements;
this._onDidUpdate.fire(this);
}
}
private static _outlineElementEquals(a: OutlineModel | OutlineGroup | OutlineElement, b: OutlineModel | OutlineGroup | OutlineElement): boolean {
if (a === b) {
return true;
} else if (!a || !b) {
return false;
} else {
return a.id === b.id;
}
}
}

View File

@@ -8,34 +8,30 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
import * as glob from 'vs/base/common/glob';
import { DisposableStore } from 'vs/base/common/lifecycle';
import { IDisposable, DisposableStore, MutableDisposable, Disposable } from 'vs/base/common/lifecycle';
import { posix } from 'vs/base/common/path';
import { basename, dirname, isEqual } from 'vs/base/common/resources';
import { URI } from 'vs/base/common/uri';
import 'vs/css!./media/breadcrumbscontrol';
import { OutlineElement, OutlineModel, TreeElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
import { IConfigurationService, IConfigurationOverrides } from 'vs/platform/configuration/common/configuration';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { FileKind, IFileService, IFileStat } from 'vs/platform/files/common/files';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { WorkbenchDataTree, WorkbenchAsyncDataTree } from 'vs/platform/list/browser/listService';
import { breadcrumbsPickerBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
import { IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { isWorkspace, isWorkspaceFolder, IWorkspace, IWorkspaceContextService, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { ResourceLabels, IResourceLabel, DEFAULT_LABELS_CONTAINER } from 'vs/workbench/browser/labels';
import { BreadcrumbsConfig } from 'vs/workbench/browser/parts/editor/breadcrumbs';
import { BreadcrumbElement, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { OutlineElement2, FileElement } from 'vs/workbench/browser/parts/editor/breadcrumbsModel';
import { IAsyncDataSource, ITreeRenderer, ITreeNode, ITreeFilter, TreeVisibility, ITreeSorter } from 'vs/base/browser/ui/tree/tree';
import { OutlineVirtualDelegate, OutlineGroupRenderer, OutlineElementRenderer, OutlineItemComparator, OutlineIdentityProvider, OutlineNavigationLabelProvider, OutlineDataSource, OutlineSortOrder, OutlineFilter, OutlineAccessibilityProvider } from 'vs/editor/contrib/documentSymbols/outlineTree';
import { IIdentityProvider, IListVirtualDelegate, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
import { IFileIconTheme, IThemeService } from 'vs/platform/theme/common/themeService';
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IModeService } from 'vs/editor/common/services/modeService';
import { localize } from 'vs/nls';
export function createBreadcrumbsPicker(instantiationService: IInstantiationService, parent: HTMLElement, element: BreadcrumbElement): BreadcrumbsPicker {
return element instanceof FileElement
? instantiationService.createInstance(BreadcrumbsFilePicker, parent)
: instantiationService.createInstance(BreadcrumbsOutlinePicker, parent);
}
import { IOutline, IOutlineComparator } from 'vs/workbench/services/outline/browser/outline';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
interface ILayoutInfo {
maxHeight: number;
@@ -62,17 +58,18 @@ export abstract class BreadcrumbsPicker {
protected _fakeEvent = new UIEvent('fakeEvent');
protected _layoutInfo!: ILayoutInfo;
private readonly _onDidPickElement = new Emitter<SelectEvent>();
readonly onDidPickElement: Event<SelectEvent> = this._onDidPickElement.event;
protected readonly _onWillPickElement = new Emitter<void>();
readonly onWillPickElement: Event<void> = this._onWillPickElement.event;
private readonly _onDidFocusElement = new Emitter<SelectEvent>();
readonly onDidFocusElement: Event<SelectEvent> = this._onDidFocusElement.event;
private readonly _previewDispoables = new MutableDisposable();
constructor(
parent: HTMLElement,
protected resource: URI,
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
@IThemeService protected readonly _themeService: IThemeService,
@IConfigurationService protected readonly _configurationService: IConfigurationService,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
) {
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-breadcrumbs-picker show-file-icons';
@@ -81,12 +78,13 @@ export abstract class BreadcrumbsPicker {
dispose(): void {
this._disposables.dispose();
this._onDidPickElement.dispose();
this._onDidFocusElement.dispose();
this._tree.dispose();
this._previewDispoables.dispose();
this._onWillPickElement.dispose();
this._domNode.remove();
setTimeout(() => this._tree.dispose(), 0); // tree cannot be disposed while being opened...
}
show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): void {
async show(input: any, maxHeight: number, width: number, arrowSize: number, arrowOffset: number): Promise<void> {
const theme = this._themeService.getColorTheme();
const color = theme.getColor(breadcrumbsPickerBackground);
@@ -103,33 +101,33 @@ export abstract class BreadcrumbsPicker {
this._domNode.appendChild(this._treeContainer);
this._layoutInfo = { maxHeight, width, arrowSize, arrowOffset, inputHeight: 0 };
this._tree = this._createTree(this._treeContainer);
this._tree = this._createTree(this._treeContainer, input);
this._disposables.add(this._tree.onDidChangeSelection(e => {
if (e.browserEvent !== this._fakeEvent) {
const target = this._getTargetFromEvent(e.elements[0]);
if (target) {
setTimeout(_ => {// need to debounce here because this disposes the tree and the tree doesn't like to be disposed on click
this._onDidPickElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') });
}, 0);
}
this._disposables.add(this._tree.onDidOpen(async e => {
const { element, editorOptions, sideBySide } = e;
const didReveal = await this._revealElement(element, { ...editorOptions, preserveFocus: false }, sideBySide);
if (!didReveal) {
return;
}
// send telemetry
interface OpenEvent { type: string }
interface OpenEventGDPR { type: { classification: 'SystemMetaData', purpose: 'FeatureInsight' } }
this._telemetryService.publicLog2<OpenEvent, OpenEventGDPR>('breadcrumbs/open', { type: element instanceof OutlineElement2 ? 'symbol' : 'file' });
}));
this._disposables.add(this._tree.onDidChangeFocus(e => {
const target = this._getTargetFromEvent(e.elements[0]);
if (target) {
this._onDidFocusElement.fire({ target, browserEvent: e.browserEvent || new UIEvent('fake') });
}
this._previewDispoables.value = this._previewElement(e.elements[0]);
}));
this._disposables.add(this._tree.onDidChangeContentHeight(() => {
this._layout();
}));
this._domNode.focus();
this._setInput(input).then(() => {
try {
await this._setInput(input);
this._layout();
}).catch(onUnexpectedError);
} catch (err) {
onUnexpectedError(err);
}
}
protected _layout(): void {
@@ -146,16 +144,15 @@ export abstract class BreadcrumbsPicker {
this._treeContainer.style.height = `${treeHeight}px`;
this._treeContainer.style.width = `${this._layoutInfo.width}px`;
this._tree.layout(treeHeight, this._layoutInfo.width);
}
get useAltAsMultipleSelectionModifier() {
return this._tree.useAltAsMultipleSelectionModifier;
}
restoreViewState(): void { }
protected abstract _setInput(element: FileElement | OutlineElement2): Promise<void>;
protected abstract _createTree(container: HTMLElement, input: any): Tree<any, any>;
protected abstract _previewElement(element: any): IDisposable;
protected abstract _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean>;
protected abstract _setInput(element: BreadcrumbElement): Promise<void>;
protected abstract _createTree(container: HTMLElement): Tree<any, any>;
protected abstract _getTargetFromEvent(element: any): any | undefined;
}
//#region - Files
@@ -173,9 +170,9 @@ class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceF
getId(element: IWorkspace | IWorkspaceFolder | IFileStat | URI): { toString(): string; } {
if (URI.isUri(element)) {
return element.toString();
} else if (IWorkspace.isIWorkspace(element)) {
} else if (isWorkspace(element)) {
return element.id;
} else if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
} else if (isWorkspaceFolder(element)) {
return element.uri.toString();
} else {
return element.resource.toString();
@@ -186,43 +183,31 @@ class FileIdentityProvider implements IIdentityProvider<IWorkspace | IWorkspaceF
class FileDataSource implements IAsyncDataSource<IWorkspace | URI, IWorkspaceFolder | IFileStat> {
private readonly _parents = new WeakMap<object, IWorkspaceFolder | IFileStat>();
constructor(
@IFileService private readonly _fileService: IFileService,
) { }
hasChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): boolean {
return URI.isUri(element)
|| IWorkspace.isIWorkspace(element)
|| IWorkspaceFolder.isIWorkspaceFolder(element)
|| isWorkspace(element)
|| isWorkspaceFolder(element)
|| element.isDirectory;
}
getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> {
if (IWorkspace.isIWorkspace(element)) {
return Promise.resolve(element.folders).then(folders => {
for (let child of folders) {
this._parents.set(element, child);
}
return folders;
});
async getChildren(element: IWorkspace | URI | IWorkspaceFolder | IFileStat): Promise<(IWorkspaceFolder | IFileStat)[]> {
if (isWorkspace(element)) {
return element.folders;
}
let uri: URI;
if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
if (isWorkspaceFolder(element)) {
uri = element.uri;
} else if (URI.isUri(element)) {
uri = element;
} else {
uri = element.resource;
}
return this._fileService.resolve(uri).then(stat => {
for (const child of stat.children || []) {
this._parents.set(stat, child);
}
return stat.children || [];
});
const stat = await this._fileService.resolve(uri);
return stat.children ?? [];
}
}
@@ -245,7 +230,7 @@ class FileRenderer implements ITreeRenderer<IFileStat | IWorkspaceFolder, FuzzyS
const { element } = node;
let resource: URI;
let fileKind: FileKind;
if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
if (isWorkspaceFolder(element)) {
resource = element.uri;
fileKind = FileKind.ROOT_FOLDER;
} else {
@@ -327,7 +312,7 @@ class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {
}
filter(element: IWorkspaceFolder | IFileStat, _parentVisibility: TreeVisibility): boolean {
if (IWorkspaceFolder.isIWorkspaceFolder(element)) {
if (isWorkspaceFolder(element)) {
// not a file
return true;
}
@@ -345,7 +330,7 @@ class FileFilter implements ITreeFilter<IWorkspaceFolder | IFileStat> {
export class FileSorter implements ITreeSorter<IFileStat | IWorkspaceFolder> {
compare(a: IFileStat | IWorkspaceFolder, b: IFileStat | IWorkspaceFolder): number {
if (IWorkspaceFolder.isIWorkspaceFolder(a) && IWorkspaceFolder.isIWorkspaceFolder(b)) {
if (isWorkspaceFolder(a) && isWorkspaceFolder(b)) {
return a.index - b.index;
}
if ((a as IFileStat).isDirectory === (b as IFileStat).isDirectory) {
@@ -363,12 +348,15 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
constructor(
parent: HTMLElement,
protected resource: URI,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IConfigurationService configService: IConfigurationService,
@IWorkspaceContextService private readonly _workspaceService: IWorkspaceContextService,
@IEditorService private readonly _editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
) {
super(parent, instantiationService, themeService, configService);
super(parent, resource, instantiationService, themeService, configService, telemetryService);
}
_createTree(container: HTMLElement) {
@@ -406,7 +394,7 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
});
}
_setInput(element: BreadcrumbElement): Promise<void> {
async _setInput(element: FileElement | OutlineElement2): Promise<void> {
const { uri, kind } = (element as FileElement);
let input: IWorkspace | URI;
if (kind === FileKind.ROOT_FOLDER) {
@@ -416,115 +404,120 @@ export class BreadcrumbsFilePicker extends BreadcrumbsPicker {
}
const tree = this._tree as WorkbenchAsyncDataTree<IWorkspace | URI, IWorkspaceFolder | IFileStat, FuzzyScore>;
return tree.setInput(input).then(() => {
let focusElement: IWorkspaceFolder | IFileStat | undefined;
for (const { element } of tree.getNode().children) {
if (IWorkspaceFolder.isIWorkspaceFolder(element) && isEqual(element.uri, uri)) {
focusElement = element;
break;
} else if (isEqual((element as IFileStat).resource, uri)) {
focusElement = element as IFileStat;
break;
}
await tree.setInput(input);
let focusElement: IWorkspaceFolder | IFileStat | undefined;
for (const { element } of tree.getNode().children) {
if (isWorkspaceFolder(element) && isEqual(element.uri, uri)) {
focusElement = element;
break;
} else if (isEqual((element as IFileStat).resource, uri)) {
focusElement = element as IFileStat;
break;
}
if (focusElement) {
tree.reveal(focusElement, 0.5);
tree.setFocus([focusElement], this._fakeEvent);
}
tree.domFocus();
});
}
if (focusElement) {
tree.reveal(focusElement, 0.5);
tree.setFocus([focusElement], this._fakeEvent);
}
tree.domFocus();
}
protected _getTargetFromEvent(element: any): any | undefined {
if (element && !IWorkspaceFolder.isIWorkspaceFolder(element) && !(element as IFileStat).isDirectory) {
return new FileElement((element as IFileStat).resource, FileKind.FILE);
protected _previewElement(_element: any): IDisposable {
return Disposable.None;
}
async _revealElement(element: IFileStat | IWorkspaceFolder, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
let resource: URI | undefined;
if (isWorkspaceFolder(element)) {
resource = element.uri;
} else if (!element.isDirectory) {
resource = element.resource;
}
if (resource) {
this._onWillPickElement.fire();
await this._editorService.openEditor({ resource, options }, sideBySide ? SIDE_GROUP : undefined);
return true;
}
return false;
}
}
//#endregion
//#region - Symbols
//#region - Outline
class OutlineTreeSorter<E> implements ITreeSorter<E> {
private _order: 'name' | 'type' | 'position';
constructor(
private comparator: IOutlineComparator<E>,
uri: URI | undefined,
@ITextResourceConfigurationService configService: ITextResourceConfigurationService,
) {
this._order = configService.getValue(uri, 'breadcrumbs.symbolSortOrder');
}
compare(a: E, b: E): number {
if (this._order === 'name') {
return this.comparator.compareByName(a, b);
} else if (this._order === 'type') {
return this.comparator.compareByType(a, b);
} else {
return this.comparator.compareByPosition(a, b);
}
}
}
export class BreadcrumbsOutlinePicker extends BreadcrumbsPicker {
protected readonly _symbolSortOrder: BreadcrumbsConfig<'position' | 'name' | 'type'>;
protected _outlineComparator: OutlineItemComparator;
protected _createTree(container: HTMLElement, input: OutlineElement2) {
constructor(
parent: HTMLElement,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IConfigurationService configurationService: IConfigurationService,
@IModeService private readonly _modeService: IModeService,
) {
super(parent, instantiationService, themeService, configurationService);
this._symbolSortOrder = BreadcrumbsConfig.SymbolSortOrder.bindTo(this._configurationService);
this._outlineComparator = new OutlineItemComparator();
}
const { config } = input.outline;
protected _createTree(container: HTMLElement) {
return <WorkbenchDataTree<OutlineModel, any, FuzzyScore>>this._instantiationService.createInstance(
return <WorkbenchDataTree<IOutline<any>, any, FuzzyScore>>this._instantiationService.createInstance(
WorkbenchDataTree,
'BreadcrumbsOutlinePicker',
container,
new OutlineVirtualDelegate(),
[new OutlineGroupRenderer(), this._instantiationService.createInstance(OutlineElementRenderer)],
new OutlineDataSource(),
config.delegate,
config.renderers,
config.treeDataSource,
{
...config.options,
sorter: this._instantiationService.createInstance(OutlineTreeSorter, config.comparator, undefined),
collapseByDefault: true,
expandOnlyOnTwistieClick: true,
multipleSelectionSupport: false,
sorter: this._outlineComparator,
identityProvider: new OutlineIdentityProvider(),
keyboardNavigationLabelProvider: new OutlineNavigationLabelProvider(),
accessibilityProvider: new OutlineAccessibilityProvider(localize('breadcrumbs', "Breadcrumbs")),
filter: this._instantiationService.createInstance(OutlineFilter, 'breadcrumbs')
}
);
}
dispose(): void {
this._symbolSortOrder.dispose();
super.dispose();
}
protected _setInput(input: OutlineElement2): Promise<void> {
protected _setInput(input: BreadcrumbElement): Promise<void> {
const element = input as TreeElement;
const model = OutlineModel.get(element)!;
const tree = this._tree as WorkbenchDataTree<OutlineModel, any, FuzzyScore>;
const viewState = input.outline.captureViewState();
this.restoreViewState = () => { viewState.dispose(); };
const overrideConfiguration = {
resource: model.uri,
overrideIdentifier: this._modeService.getModeIdByFilepathOrFirstLine(model.uri)
};
this._outlineComparator.type = this._getOutlineItemCompareType(overrideConfiguration);
const tree = this._tree as WorkbenchDataTree<IOutline<any>, any, FuzzyScore>;
tree.setInput(model);
if (element !== model) {
tree.reveal(element, 0.5);
tree.setFocus([element], this._fakeEvent);
tree.setInput(input.outline);
if (input.element !== input.outline) {
tree.reveal(input.element, 0.5);
tree.setFocus([input.element], this._fakeEvent);
}
tree.domFocus();
return Promise.resolve();
}
protected _getTargetFromEvent(element: any): any | undefined {
if (element instanceof OutlineElement) {
return element;
}
protected _previewElement(element: any): IDisposable {
const outline: IOutline<any> = this._tree.getInput();
return outline.preview(element);
}
private _getOutlineItemCompareType(overrideConfiguration?: IConfigurationOverrides): OutlineSortOrder {
switch (this._symbolSortOrder.getValue(overrideConfiguration)) {
case 'name':
return OutlineSortOrder.ByName;
case 'type':
return OutlineSortOrder.ByKind;
case 'position':
default:
return OutlineSortOrder.ByPosition;
}
async _revealElement(element: any, options: IEditorOptions, sideBySide: boolean): Promise<boolean> {
this._onWillPickElement.fire();
const outline: IOutline<any> = this._tree.getInput();
await outline.reveal(element, options, sideBySide);
return true;
}
}

View File

@@ -33,7 +33,7 @@ import {
JoinAllGroupsAction, FocusLeftGroup, FocusAboveGroup, FocusRightGroup, FocusBelowGroup, EditorLayoutSingleAction, EditorLayoutTwoColumnsAction, EditorLayoutThreeColumnsAction, EditorLayoutTwoByTwoGridAction,
EditorLayoutTwoRowsAction, EditorLayoutThreeRowsAction, EditorLayoutTwoColumnsBottomAction, EditorLayoutTwoRowsRightAction, NewEditorGroupLeftAction, NewEditorGroupRightAction,
NewEditorGroupAboveAction, NewEditorGroupBelowAction, SplitEditorOrthogonalAction, CloseEditorInAllGroupsAction, NavigateToLastEditLocationAction, ToggleGroupSizesAction, ShowAllEditorsByMostRecentlyUsedAction,
QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction
QuickAccessPreviousRecentlyUsedEditorAction, OpenPreviousRecentlyUsedEditorInGroupAction, OpenNextRecentlyUsedEditorInGroupAction, QuickAccessLeastRecentlyUsedEditorAction, QuickAccessLeastRecentlyUsedEditorInGroupAction, ReopenResourcesAction, ToggleEditorTypeAction, DuplicateGroupDownAction, DuplicateGroupLeftAction, DuplicateGroupRightAction, DuplicateGroupUpAction
} from 'vs/workbench/browser/parts/editor/editorActions';
import * as editorCommands from 'vs/workbench/browser/parts/editor/editorCommands';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
@@ -42,7 +42,7 @@ import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/co
import { ContextKeyExpr, ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { isMacintosh } from 'vs/base/common/platform';
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/parts/editor/editorWidgets';
import { OpenWorkspaceButtonContribution } from 'vs/workbench/browser/codeeditor';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { toLocalResource } from 'vs/base/common/resources';
import { Extensions as WorkbenchExtensions, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions';
@@ -351,6 +351,10 @@ registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupLeftAction,
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupRightAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.RightArrow) }), 'View: Move Editor Group Right', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupUpAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.UpArrow) }), 'View: Move Editor Group Up', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveGroupDownAction, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.DownArrow) }), 'View: Move Editor Group Down', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupLeftAction), 'View: Duplicate Editor Group Left', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupRightAction), 'View: Duplicate Editor Group Right', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupUpAction), 'View: Duplicate Editor Group Up', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(DuplicateGroupDownAction), 'View: Duplicate Editor Group Down', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToPreviousGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.LeftArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.LeftArrow } }), 'View: Move Editor into Previous Group', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToNextGroupAction, { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.RightArrow, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.RightArrow } }), 'View: Move Editor into Next Group', CATEGORIES.View.value);
registry.registerWorkbenchAction(SyncActionDescriptor.from(MoveEditorToFirstGroupAction, { primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_1, mac: { primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KEY_1 } }), 'View: Move Editor into First Group', CATEGORIES.View.value);

View File

@@ -23,6 +23,7 @@ import { AllEditorsByMostRecentlyUsedQuickAccess, ActiveGroupEditorsByMostRecent
import { Codicon } from 'vs/base/common/codicons';
import { IFilesConfigurationService, AutoSaveMode } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService';
import { openEditorWith, getAllAvailableEditors } from 'vs/workbench/services/editor/common/editorOpenWith';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
export class ExecuteCommandAction extends Action {
@@ -746,12 +747,13 @@ export class CloseEditorInAllGroupsAction extends Action {
}
}
export class BaseMoveGroupAction extends Action {
class BaseMoveCopyGroupAction extends Action {
constructor(
id: string,
label: string,
private direction: GroupDirection,
private isMove: boolean,
private editorGroupService: IEditorGroupsService
) {
super(id, label);
@@ -766,9 +768,18 @@ export class BaseMoveGroupAction extends Action {
}
if (sourceGroup) {
const targetGroup = this.findTargetGroup(sourceGroup);
if (targetGroup) {
this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction);
let resultGroup: IEditorGroup | undefined = undefined;
if (this.isMove) {
const targetGroup = this.findTargetGroup(sourceGroup);
if (targetGroup) {
resultGroup = this.editorGroupService.moveGroup(sourceGroup, targetGroup, this.direction);
}
} else {
resultGroup = this.editorGroupService.copyGroup(sourceGroup, sourceGroup, this.direction);
}
if (resultGroup) {
this.editorGroupService.activateGroup(resultGroup);
}
}
}
@@ -801,6 +812,18 @@ export class BaseMoveGroupAction extends Action {
}
}
class BaseMoveGroupAction extends BaseMoveCopyGroupAction {
constructor(
id: string,
label: string,
direction: GroupDirection,
editorGroupService: IEditorGroupsService
) {
super(id, label, direction, true, editorGroupService);
}
}
export class MoveGroupLeftAction extends BaseMoveGroupAction {
static readonly ID = 'workbench.action.moveActiveEditorGroupLeft';
@@ -857,6 +880,74 @@ export class MoveGroupDownAction extends BaseMoveGroupAction {
}
}
class BaseDuplicateGroupAction extends BaseMoveCopyGroupAction {
constructor(
id: string,
label: string,
direction: GroupDirection,
editorGroupService: IEditorGroupsService
) {
super(id, label, direction, false, editorGroupService);
}
}
export class DuplicateGroupLeftAction extends BaseDuplicateGroupAction {
static readonly ID = 'workbench.action.duplicateActiveEditorGroupLeft';
static readonly LABEL = nls.localize('duplicateActiveGroupLeft', "Duplicate Editor Group Left");
constructor(
id: string,
label: string,
@IEditorGroupsService editorGroupService: IEditorGroupsService
) {
super(id, label, GroupDirection.LEFT, editorGroupService);
}
}
export class DuplicateGroupRightAction extends BaseDuplicateGroupAction {
static readonly ID = 'workbench.action.duplicateActiveEditorGroupRight';
static readonly LABEL = nls.localize('duplicateActiveGroupRight', "Duplicate Editor Group Right");
constructor(
id: string,
label: string,
@IEditorGroupsService editorGroupService: IEditorGroupsService
) {
super(id, label, GroupDirection.RIGHT, editorGroupService);
}
}
export class DuplicateGroupUpAction extends BaseDuplicateGroupAction {
static readonly ID = 'workbench.action.duplicateActiveEditorGroupUp';
static readonly LABEL = nls.localize('duplicateActiveGroupUp', "Duplicate Editor Group Up");
constructor(
id: string,
label: string,
@IEditorGroupsService editorGroupService: IEditorGroupsService
) {
super(id, label, GroupDirection.UP, editorGroupService);
}
}
export class DuplicateGroupDownAction extends BaseDuplicateGroupAction {
static readonly ID = 'workbench.action.duplicateActiveEditorGroupDown';
static readonly LABEL = nls.localize('duplicateActiveGroupDown', "Duplicate Editor Group Down");
constructor(
id: string,
label: string,
@IEditorGroupsService editorGroupService: IEditorGroupsService
) {
super(id, label, GroupDirection.DOWN, editorGroupService);
}
}
export class MinimizeOtherGroupsAction extends Action {
static readonly ID = 'workbench.action.minimizeOtherEditors';
@@ -1803,9 +1894,8 @@ export class ReopenResourcesAction extends Action {
constructor(
id: string,
label: string,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IEditorService private readonly editorService: IEditorService,
@IConfigurationService private readonly configurationService: IConfigurationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super(id, label);
}
@@ -1823,7 +1913,7 @@ export class ReopenResourcesAction extends Action {
const options = activeEditorPane.options;
const group = activeEditorPane.group;
await openEditorWith(activeInput, undefined, options, group, this.editorService, this.configurationService, this.quickInputService);
await this.instantiationService.invokeFunction(openEditorWith, activeInput, undefined, options, group);
}
}

View File

@@ -23,7 +23,6 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { CommandsRegistry, ICommandHandler } from 'vs/platform/commands/common/commands';
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from 'vs/workbench/browser/parts/editor/editorQuickAccess';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { openEditorWith } from 'vs/workbench/services/editor/common/editorOpenWith';
@@ -484,7 +483,6 @@ function registerOpenEditorAPICommands(): void {
const editorService = accessor.get(IEditorService);
const editorGroupsService = accessor.get(IEditorGroupsService);
const configurationService = accessor.get(IConfigurationService);
const quickInputService = accessor.get(IQuickInputService);
const [columnArg, optionsArg] = columnAndOptions ?? [];
let group: IEditorGroup | undefined = undefined;
@@ -504,7 +502,7 @@ function registerOpenEditorAPICommands(): void {
const textOptions: ITextEditorOptions = optionsArg ? { ...optionsArg, override: false } : { override: false };
const input = editorService.createEditorInput({ resource: URI.revive(resource) });
return openEditorWith(input, id, textOptions, group, editorService, configurationService, quickInputService);
return openEditorWith(accessor, input, id, textOptions, group);
});
}
@@ -902,24 +900,10 @@ function registerOtherEditorCommands(): void {
id: TOGGLE_KEEP_EDITORS_COMMAND_ID,
handler: accessor => {
const configurationService = accessor.get(IConfigurationService);
const notificationService = accessor.get(INotificationService);
const openerService = accessor.get(IOpenerService);
// Update setting
const currentSetting = configurationService.getValue<boolean>('workbench.editor.enablePreview');
const newSetting = currentSetting === true ? false : true;
configurationService.updateValue('workbench.editor.enablePreview', newSetting);
// Inform user
notificationService.prompt(
Severity.Info,
newSetting ?
nls.localize('enablePreview', "Preview editors have been enabled in settings.") :
nls.localize('disablePreview', "Preview editors have been disabled in settings."),
[{
label: nls.localize('learnMode', "Learn More"), run: () => openerService.open('https://go.microsoft.com/fwlink/?linkid=2147473')
}]
);
}
});

View File

@@ -283,7 +283,8 @@ class DropOverlay extends Themable {
// Open in target group
const options = getActiveTextEditorOptions(sourceGroup, draggedEditor.editor, EditorOptions.create({
pinned: true, // always pin dropped editor
sticky: sourceGroup.isSticky(draggedEditor.editor) // preserve sticky state
sticky: sourceGroup.isSticky(draggedEditor.editor), // preserve sticky state
override: false, // Use `draggedEditor.editor` as is. If it is already a custom editor, it will stay so.
}));
const copyEditor = this.isCopyOperation(event, draggedEditor);
targetGroup.openEditor(draggedEditor.editor, options, copyEditor ? OpenEditorContext.COPY_EDITOR : OpenEditorContext.MOVE_EDITOR);

View File

@@ -322,8 +322,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
private createContainerContextMenu(): void {
const menu = this._register(this.menuService.createMenu(MenuId.EmptyEditorGroupContext, this.contextKeyService));
this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, event => this.onShowContainerContextMenu(menu, event)));
this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, event => this.onShowContainerContextMenu(menu)));
this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => this.onShowContainerContextMenu(menu, e)));
this._register(addDisposableListener(this.element, TouchEventType.Contextmenu, () => this.onShowContainerContextMenu(menu)));
}
private onShowContainerContextMenu(menu: IMenu, e?: MouseEvent): void {
@@ -1704,13 +1704,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
layout(width: number, height: number): void {
this.dimension = new Dimension(width, height);
// Ensure editor container gets height as CSS depending on the preferred height of the title control
const titleHeight = this.titleDimensions.height;
const editorHeight = Math.max(0, height - titleHeight);
this.editorContainer.style.height = `${editorHeight}px`;
// Layout the title area first to receive the size it occupies
const titleAreaSize = this.titleAreaControl.layout({
container: this.dimension,
available: new Dimension(width, height - this.editorControl.minimumHeight)
});
// Forward to controls
this.titleAreaControl.layout(new Dimension(width, titleHeight));
// Pass the container width and remaining height to the editor layout
const editorHeight = Math.max(0, height - titleAreaSize.height);
this.editorContainer.style.height = `${editorHeight}px`;
this.editorControl.layout(new Dimension(width, editorHeight));
}
@@ -1769,7 +1771,7 @@ export interface EditorReplacement {
readonly options?: EditorOptions;
}
registerThemingParticipant((theme, collector, environment) => {
registerThemingParticipant((theme, collector) => {
// Letterpress
const letterpress = `./media/letterpress${theme.type === 'dark' ? '-dark' : theme.type === 'hc' ? '-hc' : ''}.svg`;

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/editorstatus';
import * as nls from 'vs/nls';
import { localize } from 'vs/nls';
import { runAtThisOrScheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { format, compare, splitLines } from 'vs/base/common/strings';
import { extname, basename, isEqual } from 'vs/base/common/resources';
@@ -283,14 +283,15 @@ class State {
}
}
const nlsSingleSelectionRange = nls.localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)");
const nlsSingleSelection = nls.localize('singleSelection', "Ln {0}, Col {1}");
const nlsMultiSelectionRange = nls.localize('multiSelectionRange', "{0} selections ({1} characters selected)");
const nlsMultiSelection = nls.localize('multiSelection', "{0} selections");
const nlsEOLLF = nls.localize('endOfLineLineFeed', "LF");
const nlsEOLCRLF = nls.localize('endOfLineCarriageReturnLineFeed', "CRLF");
const nlsSingleSelectionRange = localize('singleSelectionRange', "Ln {0}, Col {1} ({2} selected)");
const nlsSingleSelection = localize('singleSelection', "Ln {0}, Col {1}");
const nlsMultiSelectionRange = localize('multiSelectionRange', "{0} selections ({1} characters selected)");
const nlsMultiSelection = localize('multiSelection', "{0} selections");
const nlsEOLLF = localize('endOfLineLineFeed', "LF");
const nlsEOLCRLF = localize('endOfLineCarriageReturnLineFeed', "CRLF");
export class EditorStatus extends Disposable implements IWorkbenchContribution {
private readonly tabFocusModeElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
private readonly columnSelectionModeElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
private readonly screenRedearModeElement = this._register(new MutableDisposable<IStatusbarEntryAccessor>());
@@ -342,14 +343,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
if (!this.screenReaderNotification) {
this.screenReaderNotification = this.notificationService.prompt(
Severity.Info,
nls.localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"),
localize('screenReaderDetectedExplanation.question', "Are you using a screen reader to operate VS Code? (word wrap is disabled when using a screen reader)"),
[{
label: nls.localize('screenReaderDetectedExplanation.answerYes', "Yes"),
label: localize('screenReaderDetectedExplanation.answerYes', "Yes"),
run: () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'on');
}
}, {
label: nls.localize('screenReaderDetectedExplanation.answerNo', "No"),
label: localize('screenReaderDetectedExplanation.answerNo', "No"),
run: () => {
this.configurationService.updateValue('editor.accessibilitySupport', 'off');
}
@@ -364,11 +365,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
private async showIndentationPicker(): Promise<unknown> {
const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl);
if (!activeTextEditorControl) {
return this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]);
}
if (this.editorService.activeEditor?.isReadonly()) {
return this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
}
const picks: QuickPickInput<IQuickPickItem & { run(): void; }>[] = [
@@ -390,25 +391,25 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
};
});
picks.splice(3, 0, { type: 'separator', label: nls.localize('indentConvert', "convert file") });
picks.unshift({ type: 'separator', label: nls.localize('indentView', "change view") });
picks.splice(3, 0, { type: 'separator', label: localize('indentConvert', "convert file") });
picks.unshift({ type: 'separator', label: localize('indentView', "change view") });
const action = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true });
const action = await this.quickInputService.pick(picks, { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true });
return action?.run();
}
private updateTabFocusModeElement(visible: boolean): void {
if (visible) {
if (!this.tabFocusModeElement.value) {
const text = nls.localize('tabFocusModeEnabled', "Tab Moves Focus");
const text = localize('tabFocusModeEnabled', "Tab Moves Focus");
this.tabFocusModeElement.value = this.statusbarService.addEntry({
text,
ariaLabel: text,
tooltip: nls.localize('disableTabMode', "Disable Accessibility Mode"),
tooltip: localize('disableTabMode', "Disable Accessibility Mode"),
command: 'editor.action.toggleTabFocusMode',
backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND),
color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND)
}, 'status.editor.tabFocusMode', nls.localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7);
}, 'status.editor.tabFocusMode', localize('status.editor.tabFocusMode', "Accessibility Mode"), StatusbarAlignment.RIGHT, 100.7);
}
} else {
this.tabFocusModeElement.clear();
@@ -418,15 +419,15 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
private updateColumnSelectionModeElement(visible: boolean): void {
if (visible) {
if (!this.columnSelectionModeElement.value) {
const text = nls.localize('columnSelectionModeEnabled', "Column Selection");
const text = localize('columnSelectionModeEnabled', "Column Selection");
this.columnSelectionModeElement.value = this.statusbarService.addEntry({
text,
ariaLabel: text,
tooltip: nls.localize('disableColumnSelectionMode', "Disable Column Selection Mode"),
tooltip: localize('disableColumnSelectionMode', "Disable Column Selection Mode"),
command: 'editor.action.toggleColumnSelection',
backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND),
color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND)
}, 'status.editor.columnSelectionMode', nls.localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8);
}, 'status.editor.columnSelectionMode', localize('status.editor.columnSelectionMode', "Column Selection Mode"), StatusbarAlignment.RIGHT, 100.8);
}
} else {
this.columnSelectionModeElement.clear();
@@ -436,14 +437,14 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
private updateScreenReaderModeElement(visible: boolean): void {
if (visible) {
if (!this.screenRedearModeElement.value) {
const text = nls.localize('screenReaderDetected', "Screen Reader Optimized");
const text = localize('screenReaderDetected', "Screen Reader Optimized");
this.screenRedearModeElement.value = this.statusbarService.addEntry({
text,
ariaLabel: text,
command: 'showEditorScreenReaderNotification',
backgroundColor: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_BACKGROUND),
color: themeColorFromId(STATUS_BAR_PROMINENT_ITEM_FOREGROUND)
}, 'status.editor.screenReaderMode', nls.localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6);
}, 'status.editor.screenReaderMode', localize('status.editor.screenReaderMode', "Screen Reader Mode"), StatusbarAlignment.RIGHT, 100.6);
}
} else {
this.screenRedearModeElement.clear();
@@ -459,11 +460,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('gotoLine', "Go to Line/Column"),
tooltip: localize('gotoLine', "Go to Line/Column"),
command: 'workbench.action.gotoLine'
};
this.updateElement(this.selectionElement, props, 'status.editor.selection', nls.localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5);
this.updateElement(this.selectionElement, props, 'status.editor.selection', localize('status.editor.selection', "Editor Selection"), StatusbarAlignment.RIGHT, 100.5);
}
private updateIndentationElement(text: string | undefined): void {
@@ -475,11 +476,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('selectIndentation', "Select Indentation"),
tooltip: localize('selectIndentation', "Select Indentation"),
command: 'changeEditorIndentation'
};
this.updateElement(this.indentationElement, props, 'status.editor.indentation', nls.localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4);
this.updateElement(this.indentationElement, props, 'status.editor.indentation', localize('status.editor.indentation', "Editor Indentation"), StatusbarAlignment.RIGHT, 100.4);
}
private updateEncodingElement(text: string | undefined): void {
@@ -491,11 +492,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('selectEncoding', "Select Encoding"),
tooltip: localize('selectEncoding', "Select Encoding"),
command: 'workbench.action.editor.changeEncoding'
};
this.updateElement(this.encodingElement, props, 'status.editor.encoding', nls.localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3);
this.updateElement(this.encodingElement, props, 'status.editor.encoding', localize('status.editor.encoding', "Editor Encoding"), StatusbarAlignment.RIGHT, 100.3);
}
private updateEOLElement(text: string | undefined): void {
@@ -507,11 +508,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('selectEOL', "Select End of Line Sequence"),
tooltip: localize('selectEOL', "Select End of Line Sequence"),
command: 'workbench.action.editor.changeEOL'
};
this.updateElement(this.eolElement, props, 'status.editor.eol', nls.localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2);
this.updateElement(this.eolElement, props, 'status.editor.eol', localize('status.editor.eol', "Editor End of Line"), StatusbarAlignment.RIGHT, 100.2);
}
private updateModeElement(text: string | undefined): void {
@@ -523,11 +524,11 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('selectLanguageMode', "Select Language Mode"),
tooltip: localize('selectLanguageMode', "Select Language Mode"),
command: 'workbench.action.editor.changeLanguageMode'
};
this.updateElement(this.modeElement, props, 'status.editor.mode', nls.localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1);
this.updateElement(this.modeElement, props, 'status.editor.mode', localize('status.editor.mode', "Editor Language"), StatusbarAlignment.RIGHT, 100.1);
}
private updateMetadataElement(text: string | undefined): void {
@@ -539,10 +540,10 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const props: IStatusbarEntry = {
text,
ariaLabel: text,
tooltip: nls.localize('fileInfo', "File Information")
tooltip: localize('fileInfo', "File Information")
};
this.updateElement(this.metadataElement, props, 'status.editor.info', nls.localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100);
this.updateElement(this.metadataElement, props, 'status.editor.info', localize('status.editor.info', "File Information"), StatusbarAlignment.RIGHT, 100);
}
private updateElement(element: MutableDisposable<IStatusbarEntryAccessor>, props: IStatusbarEntry, id: string, name: string, alignment: StatusbarAlignment, priority: number) {
@@ -730,8 +731,8 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
const modelOpts = model.getOptions();
update.indentation = (
modelOpts.insertSpaces
? nls.localize('spacesSize', "Spaces: {0}", modelOpts.indentSize)
: nls.localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize)
? localize('spacesSize', "Spaces: {0}", modelOpts.indentSize)
: localize({ key: 'tabSize', comment: ['Tab corresponds to the tab key'] }, "Tab Size: {0}", modelOpts.tabSize)
);
}
}
@@ -766,7 +767,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution {
if (editorWidget) {
const screenReaderDetected = this.accessibilityService.isScreenReaderOptimized();
if (screenReaderDetected) {
const screenReaderConfiguration = this.configurationService.getValue<IEditorOptions>('editor').accessibilitySupport;
const screenReaderConfiguration = this.configurationService.getValue<IEditorOptions>('editor')?.accessibilitySupport;
if (screenReaderConfiguration === 'auto') {
if (!this.promptedScreenReader) {
this.promptedScreenReader = true;
@@ -921,7 +922,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
const line = splitLines(this.currentMarker.message)[0];
const text = `${this.getType(this.currentMarker)} ${line}`;
if (!this.statusBarEntryAccessor.value) {
this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', nls.localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT);
this.statusBarEntryAccessor.value = this.statusbarService.addEntry({ text: '', ariaLabel: '' }, 'statusbar.currentProblem', localize('currentProblem', "Current Problem"), StatusbarAlignment.LEFT);
}
this.statusBarEntryAccessor.value.update({ text, ariaLabel: text });
} else {
@@ -934,9 +935,11 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
if (!currentMarker) {
return true;
}
if (!previousMarker) {
return true;
}
return IMarkerData.makeKey(previousMarker) !== IMarkerData.makeKey(currentMarker);
}
@@ -946,6 +949,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
case MarkerSeverity.Warning: return '$(warning)';
case MarkerSeverity.Info: return '$(info)';
}
return '';
}
@@ -953,17 +957,21 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
if (!this.configurationService.getValue<boolean>('problems.showCurrentInStatus')) {
return null;
}
if (!this.editor) {
return null;
}
const model = this.editor.getModel();
if (!model) {
return null;
}
const position = this.editor.getPosition();
if (!position) {
return null;
}
return this.markers.find(marker => Range.containsPosition(marker, position)) || null;
}
@@ -971,13 +979,16 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
if (!this.editor) {
return;
}
const model = this.editor.getModel();
if (!model) {
return;
}
if (model && !changedResources.some(r => isEqual(model.uri, r))) {
return;
}
this.updateMarkers();
}
@@ -985,10 +996,12 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
if (!this.editor) {
return;
}
const model = this.editor.getModel();
if (!model) {
return;
}
if (model) {
this.markers = this.markerService.read({
resource: model.uri,
@@ -998,6 +1011,7 @@ class ShowCurrentMarkerInStatusbarContribution extends Disposable {
} else {
this.markers = [];
}
this.updateStatus();
}
}
@@ -1007,9 +1021,11 @@ function compareMarker(a: IMarker, b: IMarker): number {
if (res === 0) {
res = MarkerSeverity.compare(a.severity, b.severity);
}
if (res === 0) {
res = Range.compareRangesUsingStarts(a, b);
}
return res;
}
@@ -1022,7 +1038,7 @@ export class ShowLanguageExtensionsAction extends Action {
@ICommandService private readonly commandService: ICommandService,
@IExtensionGalleryService galleryService: IExtensionGalleryService
) {
super(ShowLanguageExtensionsAction.ID, nls.localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension));
super(ShowLanguageExtensionsAction.ID, localize('showLanguageExtensions', "Search Marketplace Extensions for '{0}'...", fileExtension));
this.enabled = galleryService.isEnabled();
}
@@ -1035,7 +1051,7 @@ export class ShowLanguageExtensionsAction extends Action {
export class ChangeModeAction extends Action {
static readonly ID = 'workbench.action.editor.changeLanguageMode';
static readonly LABEL = nls.localize('changeMode', "Change Language Mode");
static readonly LABEL = localize('changeMode', "Change Language Mode");
constructor(
actionId: string,
@@ -1054,14 +1070,14 @@ export class ChangeModeAction extends Action {
async run(): Promise<void> {
const activeEditorPane = this.editorService.activeEditorPane as unknown as { isNotebookEditor?: boolean } | undefined;
if (activeEditorPane?.isNotebookEditor) {
if (activeEditorPane?.isNotebookEditor) { // TODO@rebornix TODO@jrieken debt: https://github.com/microsoft/vscode/issues/114554
// it's inside notebook editor
return this.commandService.executeCommand('notebook.cell.changeLanguage');
}
const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl);
if (!activeTextEditorControl) {
await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]);
return;
}
@@ -1085,22 +1101,24 @@ export class ChangeModeAction extends Action {
const languages = this.modeService.getRegisteredLanguageNames();
const picks: QuickPickInput[] = languages.sort().map((lang, index) => {
const modeId = this.modeService.getModeIdForLanguageName(lang.toLowerCase()) || 'unknown';
const extensions = this.modeService.getExtensions(lang).join(' ');
let description: string;
if (currentLanguageId === lang) {
description = nls.localize('languageDescription', "({0}) - Configured Language", modeId);
description = localize('languageDescription', "({0}) - Configured Language", modeId);
} else {
description = nls.localize('languageDescriptionConfigured', "({0})", modeId);
description = localize('languageDescriptionConfigured', "({0})", modeId);
}
return {
label: lang,
meta: extensions,
iconClasses: getIconClassesForModeId(modeId),
description
};
});
if (hasLanguageSupport) {
picks.unshift({ type: 'separator', label: nls.localize('languagesPicks', "languages (identifier)") });
picks.unshift({ type: 'separator', label: localize('languagesPicks', "languages (identifier)") });
}
// Offer action to configure via settings
@@ -1115,22 +1133,22 @@ export class ChangeModeAction extends Action {
picks.unshift(galleryAction);
}
configureModeSettings = { label: nls.localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) };
configureModeSettings = { label: localize('configureModeSettings', "Configure '{0}' language based settings...", currentLanguageId) };
picks.unshift(configureModeSettings);
configureModeAssociations = { label: nls.localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) };
configureModeAssociations = { label: localize('configureAssociationsExt', "Configure File Association for '{0}'...", ext) };
picks.unshift(configureModeAssociations);
}
// Offer to "Auto Detect"
const autoDetectMode: IQuickPickItem = {
label: nls.localize('autoDetect', "Auto Detect")
label: localize('autoDetect', "Auto Detect")
};
if (hasLanguageSupport) {
picks.unshift(autoDetectMode);
}
const pick = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguage', "Select Language Mode"), matchOnDescription: true });
const pick = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguage', "Select Language Mode"), matchOnDescription: true });
if (!pick) {
return;
}
@@ -1178,6 +1196,8 @@ export class ChangeModeAction extends Action {
modeSupport.setMode(languageSelection.languageIdentifier.language);
}
}
activeTextEditorControl.focus();
}
}
@@ -1194,12 +1214,12 @@ export class ChangeModeAction extends Action {
id,
label: lang,
iconClasses: getIconClassesForModeId(id),
description: (id === currentAssociation) ? nls.localize('currentAssociation', "Current Association") : undefined
description: (id === currentAssociation) ? localize('currentAssociation', "Current Association") : undefined
};
});
setTimeout(async () => {
const language = await this.quickInputService.pick(picks, { placeHolder: nls.localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) });
const language = await this.quickInputService.pick(picks, { placeHolder: localize('pickLanguageToConfigure', "Select Language Mode to Associate with '{0}'", extension || base) });
if (language) {
const fileAssociationsConfig = this.configurationService.inspect<{}>(FILES_ASSOCIATIONS_CONFIG);
@@ -1233,7 +1253,7 @@ export interface IChangeEOLEntry extends IQuickPickItem {
export class ChangeEOLAction extends Action {
static readonly ID = 'workbench.action.editor.changeEOL';
static readonly LABEL = nls.localize('changeEndOfLine', "Change End of Line Sequence");
static readonly LABEL = localize('changeEndOfLine', "Change End of Line Sequence");
constructor(
actionId: string,
@@ -1247,12 +1267,12 @@ export class ChangeEOLAction extends Action {
async run(): Promise<void> {
const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl);
if (!activeTextEditorControl) {
await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]);
return;
}
if (this.editorService.activeEditor?.isReadonly()) {
await this.quickInputService.pick([{ label: nls.localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
await this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]);
return;
}
@@ -1265,7 +1285,7 @@ export class ChangeEOLAction extends Action {
const selectedIndex = (textModel?.getEOL() === '\n') ? 0 : 1;
const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: nls.localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] });
const eol = await this.quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] });
if (eol) {
const activeCodeEditor = getCodeEditor(this.editorService.activeTextEditorControl);
if (activeCodeEditor?.hasModel() && !this.editorService.activeEditor?.isReadonly()) {
@@ -1275,13 +1295,15 @@ export class ChangeEOLAction extends Action {
textModel.pushStackElement();
}
}
activeTextEditorControl.focus();
}
}
export class ChangeEncodingAction extends Action {
static readonly ID = 'workbench.action.editor.changeEncoding';
static readonly LABEL = nls.localize('changeEncoding', "Change File Encoding");
static readonly LABEL = localize('changeEncoding', "Change File Encoding");
constructor(
actionId: string,
@@ -1296,25 +1318,26 @@ export class ChangeEncodingAction extends Action {
}
async run(): Promise<void> {
if (!getCodeEditor(this.editorService.activeTextEditorControl)) {
await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
const activeTextEditorControl = getCodeEditor(this.editorService.activeTextEditorControl);
if (!activeTextEditorControl) {
await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]);
return;
}
const activeEditorPane = this.editorService.activeEditorPane;
if (!activeEditorPane) {
await this.quickInputService.pick([{ label: nls.localize('noEditor', "No text editor active at this time") }]);
await this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]);
return;
}
const encodingSupport: IEncodingSupport | null = toEditorWithEncodingSupport(activeEditorPane.input);
if (!encodingSupport) {
await this.quickInputService.pick([{ label: nls.localize('noFileEditor', "No file active at this time") }]);
await this.quickInputService.pick([{ label: localize('noFileEditor', "No file active at this time") }]);
return;
}
const saveWithEncodingPick: IQuickPickItem = { label: nls.localize('saveWithEncoding', "Save with Encoding") };
const reopenWithEncodingPick: IQuickPickItem = { label: nls.localize('reopenWithEncoding', "Reopen with Encoding") };
const saveWithEncodingPick: IQuickPickItem = { label: localize('saveWithEncoding', "Save with Encoding") };
const reopenWithEncodingPick: IQuickPickItem = { label: localize('reopenWithEncoding', "Reopen with Encoding") };
if (!Language.isDefaultVariant()) {
const saveWithEncodingAlias = 'Save with Encoding';
@@ -1334,7 +1357,7 @@ export class ChangeEncodingAction extends Action {
} else if (activeEditorPane.input.isReadonly()) {
action = reopenWithEncodingPick;
} else {
action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: nls.localize('pickAction', "Select Action"), matchOnDetail: true });
action = await this.quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true });
}
if (!action) {
@@ -1394,11 +1417,11 @@ export class ChangeEncodingAction extends Action {
// If we have a guessed encoding, show it first unless it matches the configured encoding
if (guessedEncoding && configuredEncoding !== guessedEncoding && SUPPORTED_ENCODINGS[guessedEncoding]) {
picks.unshift({ type: 'separator' });
picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: nls.localize('guessedEncoding', "Guessed from content") });
picks.unshift({ id: guessedEncoding, label: SUPPORTED_ENCODINGS[guessedEncoding].labelLong, description: localize('guessedEncoding', "Guessed from content") });
}
const encoding = await this.quickInputService.pick(picks, {
placeHolder: isReopenWithEncoding ? nls.localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : nls.localize('pickEncodingForSave', "Select File Encoding to Save with"),
placeHolder: isReopenWithEncoding ? localize('pickEncodingForReopen', "Select File Encoding to Reopen File") : localize('pickEncodingForSave', "Select File Encoding to Save with"),
activeItem: items[typeof directMatchIndex === 'number' ? directMatchIndex : typeof aliasMatchIndex === 'number' ? aliasMatchIndex : -1]
});
@@ -1414,5 +1437,7 @@ export class ChangeEncodingAction extends Action {
if (typeof encoding.id !== 'undefined' && activeEncodingSupport && activeEncodingSupport.getEncoding() !== encoding.id) {
activeEncodingSupport.setEncoding(encoding.id, isReopenWithEncoding ? EncodingMode.Decode : EncodingMode.Encode); // Set new encoding
}
activeTextEditorControl.focus();
}
}

View File

@@ -14,7 +14,7 @@
flex: auto;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .title-label {
.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label {
line-height: 35px;
overflow: hidden;
text-overflow: ellipsis;
@@ -22,16 +22,16 @@
padding-left: 20px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label {
flex: none;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before {
height: 35px; /* tweak the icon size of the editor labels when icons are enabled */
.monaco-workbench .part.editor > .content .editor-group-container > .title > .label-container > .title-label > .monaco-icon-label-container {
flex: none; /* helps to show decorations right next to the label and not at the end */
}
/* Breadcrumbs */
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .no-tabs.title-label {
flex: none;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control {
flex: 1 50%;
overflow: hidden;
@@ -62,13 +62,11 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder + .monaco-breadcrumb-item::before,
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control.relative-path .monaco-breadcrumb-item:nth-child(2)::before,
.monaco-workbench.windows .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:nth-child(2)::before {
/* workspace folder, item following workspace folder, or relative path -> hide first seperator */
display: none;
display: none; /* workspace folder, item following workspace folder, or relative path -> hide first seperator */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item.root_folder::after {
/* use dot separator for workspace folder */
content: '\00a0•\00a0';
content: '\00a0•\00a0'; /* use dot separator for workspace folder */
padding: 0;
}
@@ -80,13 +78,18 @@
padding: 0 1px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child {
display: none; /* hides chevrons when no tabs visible and when last items */
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item .codicon:last-child {
display: none; /* hides chevrons when no tabs visible */
}
/* Title Actions */
.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions {
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .breadcrumbs-control .monaco-icon-label::before {
height: 18px;
padding-right: 2px;
}
/* Editor Actions Toolbar (via title actions) */
.monaco-workbench .part.editor > .content .editor-group-container > .title > .title-actions {
display: flex;
flex: initial;
opacity: 0.5;
@@ -94,6 +97,6 @@
height: 35px;
}
.monaco-workbench .part.editor > .content .editor-group-container.active > .title .title-actions {
.monaco-workbench .part.editor > .content .editor-group-container.active > .title > .title-actions {
opacity: 1;
}

View File

@@ -3,17 +3,45 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/*
################################### z-index explainer ###################################
Tabs have various levels of z-index depending on state, typically:
- scrollbar should be above all
- sticky (compact, shrink) tabs need to be above non-sticky tabs for scroll under effect
including non-sticky tabs-top borders, otherwise these borders would not scroll under
(https://github.com/microsoft/vscode/issues/111641)
- bottom-border needs to be above tabs bottom border to win but also support sticky tabs
(https://github.com/microsoft/vscode/issues/99084) <- this currently cannot be done and
is still broken. putting sticky-tabs above tabs bottom border would not render this
border at all for sticky tabs.
On top of that there is 2 borders with a z-index for a general border below tabs
- tabs bottom border
- editor title bottom border (when breadcrumbs are disabled, this border will appear
same as tabs bottom border)
The following tabls shows the current stacking order:
[z-index] [kind]
7 scrollbar
6 active-tab border-bottom
5 tabs, title border bottom
4 sticky-tab
2 active/dirty-tab border top
0 tab
##########################################################################################
*/
/* Title Container */
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container {
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container {
display: flex;
position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom {
position: relative;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after {
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.tabs-border-bottom::after {
content: '';
position: absolute;
bottom: 0;
@@ -25,12 +53,12 @@
height: 1px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element {
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element {
flex: 1;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar {
z-index: 3; /* on top of tabs */
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container > .monaco-scrollable-element .scrollbar {
z-index: 7;
cursor: default;
}
@@ -46,6 +74,13 @@
overflow: scroll !important;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container {
/* Enable wrapping via flex layout and dynamic height */
height: auto;
flex-wrap: wrap;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar {
display: none; /* Chrome + Safari: hide scrollbar */
}
@@ -62,6 +97,10 @@
padding-left: 10px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child {
margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) {
padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */
@@ -74,6 +113,10 @@
flex-shrink: 0;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit {
flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink {
min-width: 80px;
flex-basis: 0; /* all tabs are even */
@@ -89,7 +132,7 @@
/** Sticky compact/shrink tabs do not scroll in case of overflow and are always above unsticky tabs which scroll under */
position: sticky;
z-index: 1;
z-index: 4;
/** Sticky compact/shrink tabs are even and never grow */
flex-basis: 0;
@@ -118,9 +161,7 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink,
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink {
/** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */
position: static;
position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label {
@@ -132,7 +173,7 @@
content: '';
display: flex;
flex: 0;
width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */
width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left {
@@ -167,24 +208,26 @@
display: block;
position: absolute;
left: 0;
z-index: 6; /* over possible title border */
pointer-events: none;
width: 100%;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-top > .tab-border-top-container {
z-index: 2;
top: 0;
height: 1px;
background-color: var(--tab-border-top-color);
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.active.tab-border-bottom > .tab-border-bottom-container {
z-index: 6;
bottom: 0;
height: 1px;
background-color: var(--tab-border-bottom-color);
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty-border-top > .tab-border-top-container {
z-index: 2;
top: 0;
height: 2px;
background-color: var(--tab-dirty-border-top-color);
@@ -203,13 +246,16 @@
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink > .tab-label::after {
content: '';
content: ''; /* enables a linear gradient to overlay the end of the label when tabs overflow */
position: absolute;
right: 0;
height: 100%;
width: 5px;
opacity: 1;
padding: 0;
/* the rules below ensure that the gradient does not impact top/bottom borders (https://github.com/microsoft/vscode/issues/115129) */
top: 1px;
bottom: 1px;
height: calc(100% - 2px);
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink:focus > .tab-label::after {
@@ -243,7 +289,7 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions {
flex: 0;
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */
overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions,
@@ -301,7 +347,7 @@
margin-right: 0.5em;
}
/* No Tab Actions */
/* Tab Actions: Off */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-off {
padding-right: 10px; /* give a little bit more room if tab actions is off */
@@ -324,15 +370,6 @@
pointer-events: none; /* don't allow tab actions to be clicked when running without tab actions */
}
/* Editor Actions */
.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions {
cursor: default;
flex: initial;
padding: 0 8px 0 4px;
height: 35px;
}
/* Breadcrumbs */
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control {
@@ -350,11 +387,17 @@
height: 22px; /* tweak the icon size of the editor labels when icons are enabled */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item {
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .outline-element-icon {
padding-right: 3px;
height: 22px; /* tweak the icon size of the editor labels when icons are enabled */
line-height: 22px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item {
max-width: 80%;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before {
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item::before {
width: 16px;
height: 22px;
display: flex;
@@ -362,6 +405,27 @@
justify-content: center;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child {
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child {
padding-right: 8px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control .monaco-breadcrumb-item:last-child .codicon:last-child {
display: none; /* hides chevrons when last item */
}
/* Editor Actions Toolbar */
.monaco-workbench .part.editor > .content .editor-group-container > .title .editor-actions {
cursor: default;
flex: initial;
padding: 0 8px 0 4px;
height: 35px;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions {
/* When tabs are wrapped, position the editor actions at the end of the very last row */
position: absolute;
bottom: 0;
right: 0;
}

View File

@@ -26,6 +26,15 @@
cursor: pointer;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title .monaco-icon-label::before {
height: 35px; /* tweak the icon size of the editor labels when icons are enabled */
}
.monaco-workbench .part.editor > .content .editor-group-container > .title.breadcrumbs .monaco-icon-label::after,
.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs .monaco-icon-label::after {
padding-right: 0; /* by default the icon label has a padding right and this isn't wanted when not showing tabs and not showing breadcrumbs */
}
/* Title Actions */
.monaco-workbench .part.editor > .content .editor-group-container > .title .title-actions .action-label:not(span),
@@ -59,13 +68,12 @@
opacity: 0.4;
}
/* Drag Cursor */
/* Drag and Drop */
.monaco-workbench .part.editor > .content .editor-group-container > .title {
cursor: grab;
}
/* Drag and Drop Feedback */
.monaco-editor-group-drag-image {
display: inline-block;
padding: 1px 7px;

View File

@@ -5,7 +5,7 @@
import 'vs/css!./media/notabstitlecontrol';
import { EditorResourceAccessor, Verbosity, IEditorInput, IEditorPartOptions, SideBySideEditor } from 'vs/workbench/common/editor';
import { TitleControl, IToolbarActions } from 'vs/workbench/browser/parts/editor/titleControl';
import { TitleControl, IToolbarActions, ITitleControlDimensions } from 'vs/workbench/browser/parts/editor/titleControl';
import { ResourceLabel, IResourceLabel } from 'vs/workbench/browser/labels';
import { TAB_ACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND } from 'vs/workbench/common/theme';
import { EventType as TouchEventType, GestureEvent, Gesture } from 'vs/base/browser/touch';
@@ -15,6 +15,7 @@ import { CLOSE_EDITOR_COMMAND_ID } from 'vs/workbench/browser/parts/editor/edito
import { Color } from 'vs/base/common/color';
import { withNullAsUndefined, assertIsDefined, assertAllDefined } from 'vs/base/common/types';
import { IEditorGroupTitleDimensions } from 'vs/workbench/browser/parts/editor/editor';
import { equals } from 'vs/base/common/objects';
interface IRenderedEditorLabel {
editor?: IEditorInput;
@@ -50,7 +51,7 @@ export class NoTabsTitleControl extends TitleControl {
// Breadcrumbs
this.createBreadcrumbsControl(labelContainer, { showFileIcons: false, showSymbolIcons: true, showDecorationColors: false, breadcrumbsBackground: () => Color.transparent });
titleContainer.classList.toggle('breadcrumbs', Boolean(this.breadcrumbsControl));
this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // import to remove because the container is a shared dom node
this._register({ dispose: () => titleContainer.classList.remove('breadcrumbs') }); // important to remove because the container is a shared dom node
// Right Actions Container
const actionsContainer = document.createElement('div');
@@ -67,16 +68,16 @@ export class NoTabsTitleControl extends TitleControl {
this.enableGroupDragging(titleContainer);
// Pin on double click
this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, (e: MouseEvent) => this.onTitleDoubleClick(e)));
this._register(addDisposableListener(titleContainer, EventType.DBLCLICK, e => this.onTitleDoubleClick(e)));
// Detect mouse click
this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, (e: MouseEvent) => this.onTitleAuxClick(e)));
this._register(addDisposableListener(titleContainer, EventType.AUXCLICK, e => this.onTitleAuxClick(e)));
// Detect touch
this._register(addDisposableListener(titleContainer, TouchEventType.Tap, (e: GestureEvent) => this.onTitleTap(e)));
// Context Menu
this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, (e: Event) => {
this._register(addDisposableListener(titleContainer, EventType.CONTEXT_MENU, e => {
if (this.group.activeEditor) {
this.onContextMenu(this.group.activeEditor, e, titleContainer);
}
@@ -188,7 +189,7 @@ export class NoTabsTitleControl extends TitleControl {
}
updateOptions(oldOptions: IEditorPartOptions, newOptions: IEditorPartOptions): void {
if (oldOptions.labelFormat !== newOptions.labelFormat) {
if (oldOptions.labelFormat !== newOptions.labelFormat || !equals(oldOptions.decorations, newOptions.decorations)) {
this.redraw();
}
}
@@ -236,6 +237,7 @@ export class NoTabsTitleControl extends TitleControl {
private redraw(): void {
const editor = withNullAsUndefined(this.group.activeEditor);
const options = this.accessor.partOptions;
const isEditorPinned = editor ? this.group.isPinned(editor) : false;
const isGroupActive = this.accessor.activeGroup === this.group;
@@ -291,14 +293,18 @@ export class NoTabsTitleControl extends TitleControl {
{
title,
italic: !isEditorPinned,
extraClasses: ['no-tabs', 'title-label']
extraClasses: ['no-tabs', 'title-label'],
fileDecorations: {
colors: Boolean(options.decorations?.colors),
badges: Boolean(options.decorations?.badges)
},
}
);
if (isGroupActive) {
editorLabel.element.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || '';
titleContainer.style.color = this.getColor(TAB_ACTIVE_FOREGROUND) || '';
} else {
editorLabel.element.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || '';
titleContainer.style.color = this.getColor(TAB_UNFOCUSED_ACTIVE_FOREGROUND) || '';
}
// Update Editor Actions Toolbar
@@ -333,9 +339,11 @@ export class NoTabsTitleControl extends TitleControl {
};
}
layout(dimension: Dimension): void {
layout(dimensions: ITitleControlDimensions): Dimension {
if (this.breadcrumbsControl) {
this.breadcrumbsControl.layout(undefined);
}
return new Dimension(dimensions.container.width, this.getDimensions().height);
}
}

View File

@@ -1,121 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { Emitter } from 'vs/base/common/event';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IRange } from 'vs/editor/common/core/range';
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
import { ICodeEditor, isCodeEditor, isCompositeEditor } from 'vs/editor/browser/editorBrowser';
import { TrackedRangeStickiness, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
import { isEqual } from 'vs/base/common/resources';
export interface IRangeHighlightDecoration {
resource: URI;
range: IRange;
isWholeLine?: boolean;
}
export class RangeHighlightDecorations extends Disposable {
private rangeHighlightDecorationId: string | null = null;
private editor: ICodeEditor | null = null;
private readonly editorDisposables = this._register(new DisposableStore());
private readonly _onHighlightRemoved: Emitter<void> = this._register(new Emitter<void>());
readonly onHighlightRemoved = this._onHighlightRemoved.event;
constructor(
@IEditorService private readonly editorService: IEditorService
) {
super();
}
removeHighlightRange() {
if (this.editor && this.editor.getModel() && this.rangeHighlightDecorationId) {
this.editor.deltaDecorations([this.rangeHighlightDecorationId], []);
this._onHighlightRemoved.fire();
}
this.rangeHighlightDecorationId = null;
}
highlightRange(range: IRangeHighlightDecoration, editor?: any) {
editor = editor ?? this.getEditor(range);
if (isCodeEditor(editor)) {
this.doHighlightRange(editor, range);
} else if (isCompositeEditor(editor) && isCodeEditor(editor.activeCodeEditor)) {
this.doHighlightRange(editor.activeCodeEditor, range);
}
}
private doHighlightRange(editor: ICodeEditor, selectionRange: IRangeHighlightDecoration) {
this.removeHighlightRange();
editor.changeDecorations((changeAccessor: IModelDecorationsChangeAccessor) => {
this.rangeHighlightDecorationId = changeAccessor.addDecoration(selectionRange.range, this.createRangeHighlightDecoration(selectionRange.isWholeLine));
});
this.setEditor(editor);
}
private getEditor(resourceRange: IRangeHighlightDecoration): ICodeEditor | undefined {
const activeEditor = this.editorService.activeEditor;
const resource = activeEditor && activeEditor.resource;
if (resource && isEqual(resource, resourceRange.resource)) {
return this.editorService.activeTextEditorControl as ICodeEditor;
}
return undefined;
}
private setEditor(editor: ICodeEditor) {
if (this.editor !== editor) {
this.editorDisposables.clear();
this.editor = editor;
this.editorDisposables.add(this.editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
if (
e.reason === CursorChangeReason.NotSet
|| e.reason === CursorChangeReason.Explicit
|| e.reason === CursorChangeReason.Undo
|| e.reason === CursorChangeReason.Redo
) {
this.removeHighlightRange();
}
}));
this.editorDisposables.add(this.editor.onDidChangeModel(() => { this.removeHighlightRange(); }));
this.editorDisposables.add(this.editor.onDidDispose(() => {
this.removeHighlightRange();
this.editor = null;
}));
}
}
private static readonly _WHOLE_LINE_RANGE_HIGHLIGHT = ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'rangeHighlight',
isWholeLine: true
});
private static readonly _RANGE_HIGHLIGHT = ModelDecorationOptions.register({
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
className: 'rangeHighlight'
});
private createRangeHighlightDecoration(isWholeLine: boolean = true): ModelDecorationOptions {
return (isWholeLine ? RangeHighlightDecorations._WHOLE_LINE_RANGE_HIGHLIGHT : RangeHighlightDecorations._RANGE_HIGHLIGHT);
}
dispose() {
super.dispose();
if (this.editor && this.editor.getModel()) {
this.removeHighlightRange();
this.editor = null;
}
}
}

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