diff --git a/extensions/typescript-language-features/src/tsServer/versionManager.ts b/extensions/typescript-language-features/src/tsServer/versionManager.ts index edf1540252d..861ffdd05b0 100644 --- a/extensions/typescript-language-features/src/tsServer/versionManager.ts +++ b/extensions/typescript-language-features/src/tsServer/versionManager.ts @@ -38,16 +38,11 @@ export class TypeScriptVersionManager extends Disposable { this._currentVersion = localVersion; } } else { - setImmediate(() => { - vscode.workspace.requestWorkspaceTrust({ modal: false }) - .then(trusted => { - if (trusted && this.versionProvider.localVersion) { - this.updateActiveVersion(this.versionProvider.localVersion); - } else { - this.updateActiveVersion(this.versionProvider.defaultVersion); - } - }); - }); + this._disposables.push(vscode.workspace.onDidReceiveWorkspaceTrust(() => { + if (this.versionProvider.localVersion) { + this.updateActiveVersion(this.versionProvider.localVersion); + } + })); } } diff --git a/src/vs/base/browser/ui/button/button.css b/src/vs/base/browser/ui/button/button.css index 7ecbcb9f335..fb4ebbf081d 100644 --- a/src/vs/base/browser/ui/button/button.css +++ b/src/vs/base/browser/ui/button/button.css @@ -40,3 +40,15 @@ .monaco-button-dropdown > .monaco-dropdown-button { margin-left: 1px; } + +.monaco-description-button { + flex-direction: column; +} + +.monaco-description-button .monaco-button-label { + font-weight: 500; +} + +.monaco-description-button .monaco-button-description { + font-style: italic; +} diff --git a/src/vs/base/browser/ui/button/button.ts b/src/vs/base/browser/ui/button/button.ts index 473eb2a2ec4..e39f90d3296 100644 --- a/src/vs/base/browser/ui/button/button.ts +++ b/src/vs/base/browser/ui/button/button.ts @@ -50,6 +50,10 @@ export interface IButton extends IDisposable { hasFocus(): boolean; } +export interface IButtonWithDescription extends IButton { + description: string; +} + export class Button extends Disposable implements IButton { private _element: HTMLElement; @@ -303,6 +307,207 @@ export class ButtonWithDropdown extends Disposable implements IButton { } } +export class ButtonWithDescription extends Disposable implements IButtonWithDescription { + + private _element: HTMLElement; + private _labelElement: HTMLElement; + private _descriptionElement: HTMLElement; + private options: IButtonOptions; + + private buttonBackground: Color | undefined; + private buttonHoverBackground: Color | undefined; + private buttonForeground: Color | undefined; + private buttonSecondaryBackground: Color | undefined; + private buttonSecondaryHoverBackground: Color | undefined; + private buttonSecondaryForeground: Color | undefined; + private buttonBorder: Color | undefined; + + private _onDidClick = this._register(new Emitter()); + get onDidClick(): BaseEvent { return this._onDidClick.event; } + + private focusTracker: IFocusTracker; + + constructor(container: HTMLElement, options?: IButtonOptions) { + super(); + + this.options = options || Object.create(null); + mixin(this.options, defaultOptions, false); + + this.buttonForeground = this.options.buttonForeground; + this.buttonBackground = this.options.buttonBackground; + this.buttonHoverBackground = this.options.buttonHoverBackground; + + this.buttonSecondaryForeground = this.options.buttonSecondaryForeground; + this.buttonSecondaryBackground = this.options.buttonSecondaryBackground; + this.buttonSecondaryHoverBackground = this.options.buttonSecondaryHoverBackground; + + this.buttonBorder = this.options.buttonBorder; + + this._element = document.createElement('a'); + this._element.classList.add('monaco-button'); + this._element.classList.add('monaco-description-button'); + this._element.tabIndex = 0; + this._element.setAttribute('role', 'button'); + + this._labelElement = document.createElement('div'); + this._labelElement.classList.add('monaco-button-label'); + this._labelElement.tabIndex = -1; + this._element.appendChild(this._labelElement); + + this._descriptionElement = document.createElement('div'); + this._descriptionElement.classList.add('monaco-button-description'); + this._descriptionElement.tabIndex = -1; + this._element.appendChild(this._descriptionElement); + + container.appendChild(this._element); + + this._register(Gesture.addTarget(this._element)); + + [EventType.CLICK, TouchEventType.Tap].forEach(eventType => { + this._register(addDisposableListener(this._element, eventType, e => { + if (!this.enabled) { + EventHelper.stop(e); + return; + } + + this._onDidClick.fire(e); + })); + }); + + this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + let eventHandled = false; + if (this.enabled && (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space))) { + this._onDidClick.fire(e); + eventHandled = true; + } else if (event.equals(KeyCode.Escape)) { + this._element.blur(); + eventHandled = true; + } + + if (eventHandled) { + EventHelper.stop(event, true); + } + })); + + this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => { + if (!this._element.classList.contains('disabled')) { + this.setHoverBackground(); + } + })); + + this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => { + this.applyStyles(); // restore standard styles + })); + + // Also set hover background when button is focused for feedback + this.focusTracker = this._register(trackFocus(this._element)); + this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground())); + this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles + + this.applyStyles(); + } + + private setHoverBackground(): void { + let hoverBackground; + if (this.options.secondary) { + hoverBackground = this.buttonSecondaryHoverBackground ? this.buttonSecondaryHoverBackground.toString() : null; + } else { + hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null; + } + if (hoverBackground) { + this._element.style.backgroundColor = hoverBackground; + } + } + + style(styles: IButtonStyles): void { + this.buttonForeground = styles.buttonForeground; + this.buttonBackground = styles.buttonBackground; + this.buttonHoverBackground = styles.buttonHoverBackground; + this.buttonSecondaryForeground = styles.buttonSecondaryForeground; + this.buttonSecondaryBackground = styles.buttonSecondaryBackground; + this.buttonSecondaryHoverBackground = styles.buttonSecondaryHoverBackground; + this.buttonBorder = styles.buttonBorder; + + this.applyStyles(); + } + + private applyStyles(): void { + if (this._element) { + let background, foreground; + if (this.options.secondary) { + foreground = this.buttonSecondaryForeground ? this.buttonSecondaryForeground.toString() : ''; + background = this.buttonSecondaryBackground ? this.buttonSecondaryBackground.toString() : ''; + } else { + foreground = this.buttonForeground ? this.buttonForeground.toString() : ''; + background = this.buttonBackground ? this.buttonBackground.toString() : ''; + } + + const border = this.buttonBorder ? this.buttonBorder.toString() : ''; + + this._element.style.color = foreground; + this._element.style.backgroundColor = background; + + this._element.style.borderWidth = border ? '1px' : ''; + this._element.style.borderStyle = border ? 'solid' : ''; + this._element.style.borderColor = border; + } + } + + get element(): HTMLElement { + return this._element; + } + + set label(value: string) { + this._element.classList.add('monaco-text-button'); + if (this.options.supportIcons) { + reset(this._labelElement, ...renderLabelWithIcons(value)); + } else { + this._labelElement.textContent = value; + } + if (typeof this.options.title === 'string') { + this._element.title = this.options.title; + } else if (this.options.title) { + this._element.title = value; + } + } + + set description(value: string) { + if (this.options.supportIcons) { + reset(this._descriptionElement, ...renderLabelWithIcons(value)); + } else { + this._descriptionElement.textContent = value; + } + } + + set icon(icon: CSSIcon) { + this._element.classList.add(...CSSIcon.asClassNameArray(icon)); + } + + set enabled(value: boolean) { + if (value) { + this._element.classList.remove('disabled'); + this._element.setAttribute('aria-disabled', String(false)); + this._element.tabIndex = 0; + } else { + this._element.classList.add('disabled'); + this._element.setAttribute('aria-disabled', String(true)); + } + } + + get enabled() { + return !this._element.classList.contains('disabled'); + } + + focus(): void { + this._element.focus(); + } + + hasFocus(): boolean { + return this._element === document.activeElement; + } +} + export class ButtonBar extends Disposable { private _buttons: IButton[] = []; @@ -321,6 +526,12 @@ export class ButtonBar extends Disposable { return button; } + addButtonWithDescription(options?: IButtonOptions): IButtonWithDescription { + const button = this._register(new ButtonWithDescription(this.container, options)); + this.pushButton(button); + return button; + } + addButtonWithDropdown(options: IButtonWithDropdownOptions): IButton { const button = this._register(new ButtonWithDropdown(this.container, options)); this.pushButton(button); diff --git a/src/vs/base/browser/ui/dialog/dialog.css b/src/vs/base/browser/ui/dialog/dialog.css index 98cd14eabe3..ff0cff5a0e0 100644 --- a/src/vs/base/browser/ui/dialog/dialog.css +++ b/src/vs/base/browser/ui/dialog/dialog.css @@ -34,6 +34,7 @@ /** Dialog: Title Actions Row */ .monaco-dialog-box .dialog-toolbar-row { + height: 22px; padding-bottom: 4px; } @@ -138,6 +139,10 @@ overflow: hidden; } +.monaco-dialog-box > .dialog-buttons-row > .dialog-buttons.centered { + justify-content: center; +} + .monaco-dialog-box > .dialog-buttons-row > .dialog-buttons > .monaco-button { width: fit-content; width: -moz-fit-content; diff --git a/src/vs/base/browser/ui/dialog/dialog.ts b/src/vs/base/browser/ui/dialog/dialog.ts index 79129138217..74912e178b1 100644 --- a/src/vs/base/browser/ui/dialog/dialog.ts +++ b/src/vs/base/browser/ui/dialog/dialog.ts @@ -11,7 +11,7 @@ import { domEvent } from 'vs/base/browser/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Color } from 'vs/base/common/color'; -import { ButtonBar, IButtonStyles } from 'vs/base/browser/ui/button/button'; +import { ButtonBar, ButtonWithDescription, IButtonStyles } from 'vs/base/browser/ui/button/button'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { Action } from 'vs/base/common/actions'; import { mnemonicButtonLabel } from 'vs/base/common/labels'; @@ -36,6 +36,8 @@ export interface IDialogOptions { readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void; readonly renderBody?: (container: HTMLElement) => void; readonly icon?: Codicon; + readonly buttonDetails?: string[]; + readonly disableCloseAction?: boolean; } export interface IDialogResult { @@ -55,6 +57,8 @@ export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles { readonly inputBackground?: Color; readonly inputForeground?: Color; readonly inputBorder?: Color; + readonly textLinkForeground?: Color; + } interface ButtonMapEntry { @@ -73,6 +77,7 @@ export class Dialog extends Disposable { private modalElement: HTMLElement | undefined; private readonly buttonsContainer: HTMLElement; private readonly messageDetailElement: HTMLElement; + private readonly messageContainer: HTMLElement; private readonly iconElement: HTMLElement; private readonly checkbox: SimpleCheckbox | undefined; private readonly toolbarContainer: HTMLElement; @@ -97,15 +102,15 @@ export class Dialog extends Disposable { const messageRowElement = this.element.appendChild($('.dialog-message-row')); this.iconElement = messageRowElement.appendChild($('.dialog-icon')); - const messageContainer = messageRowElement.appendChild($('.dialog-message-container')); + this.messageContainer = messageRowElement.appendChild($('.dialog-message-container')); if (this.options.detail || this.options.renderBody) { - const messageElement = messageContainer.appendChild($('.dialog-message')); + const messageElement = this.messageContainer.appendChild($('.dialog-message')); const messageTextElement = messageElement.appendChild($('.dialog-message-text')); messageTextElement.innerText = this.message; } - this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail')); + this.messageDetailElement = this.messageContainer.appendChild($('.dialog-message-detail')); if (this.options.detail || !this.options.renderBody) { this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; } else { @@ -113,13 +118,13 @@ export class Dialog extends Disposable { } if (this.options.renderBody) { - const customBody = messageContainer.appendChild($('.dialog-message-body')); + const customBody = this.messageContainer.appendChild($('.dialog-message-body')); this.options.renderBody(customBody); } if (this.options.inputs) { this.inputs = this.options.inputs.map(input => { - const inputRowElement = messageContainer.appendChild($('.dialog-message-input')); + const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input')); const inputBox = this._register(new InputBox(inputRowElement, undefined, { placeholder: input.placeholder, @@ -137,7 +142,7 @@ export class Dialog extends Disposable { } if (this.options.checkboxLabel) { - const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row')); + const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row')); const checkbox = this.checkbox = this._register(new SimpleCheckbox(this.options.checkboxLabel, !!this.options.checkboxChecked)); @@ -186,12 +191,16 @@ export class Dialog extends Disposable { const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer)); const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId); + this.buttonsContainer.classList.toggle('centered'); // Handle button clicks buttonMap.forEach((entry, index) => { - const button = this._register(buttonBar.addButton({ title: true })); + const primary = buttonMap[index].index === 0; + const button = this.options.buttonDetails ? this._register(buttonBar.addButtonWithDescription({ title: true, secondary: !primary })) : this._register(buttonBar.addButton({ title: true, secondary: !primary })); button.label = mnemonicButtonLabel(buttonMap[index].label, true); - + if (button instanceof ButtonWithDescription) { + button.description = this.options.buttonDetails![buttonMap[index].index]; + } this._register(button.onDidClick(e => { if (e) { EventHelper.stop(e); @@ -298,7 +307,7 @@ export class Dialog extends Disposable { EventHelper.stop(e, true); const evt = new StandardKeyboardEvent(e); - if (evt.equals(KeyCode.Escape)) { + if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) { resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined @@ -346,16 +355,19 @@ export class Dialog extends Disposable { } } - const actionBar = this._register(new ActionBar(this.toolbarContainer, {})); - const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => { - resolve({ - button: this.options.cancelId || 0, - checkboxChecked: this.checkbox ? this.checkbox.checked : undefined - }); - })); + if (!this.options.disableCloseAction) { + const actionBar = this._register(new ActionBar(this.toolbarContainer, {})); - actionBar.push(action, { icon: true, label: false, }); + const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => { + resolve({ + button: this.options.cancelId || 0, + checkboxChecked: this.checkbox ? this.checkbox.checked : undefined + }); + })); + + actionBar.push(action, { icon: true, label: false, }); + } this.applyStyles(); @@ -384,6 +396,7 @@ export class Dialog extends Disposable { const bgColor = style.dialogBackground; const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : ''; const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : ''; + const linkFgColor = style.textLinkForeground; this.shadowElement.style.boxShadow = shadowColor; @@ -404,6 +417,12 @@ export class Dialog extends Disposable { this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString(); } + if (linkFgColor) { + for (const el of this.messageContainer.getElementsByTagName('a')) { + el.style.color = linkFgColor.toString(); + } + } + let color; switch (this.options.type) { case 'error': diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index 7aa108fa9c8..9e3738404d0 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -181,9 +181,11 @@ export interface IOpenDialogOptions { export const IDialogService = createDecorator('dialogService'); export interface ICustomDialogOptions { - markdownDetails?: ICustomDialogMarkdown[] + buttonDetails?: string[]; + markdownDetails?: ICustomDialogMarkdown[]; classes?: string[]; icon?: Codicon; + disableCloseAction?: boolean; } export interface ICustomDialogMarkdown { diff --git a/src/vs/platform/theme/common/styler.ts b/src/vs/platform/theme/common/styler.ts index 05fa1c194f3..1a37e36729d 100644 --- a/src/vs/platform/theme/common/styler.ts +++ b/src/vs/platform/theme/common/styler.ts @@ -324,7 +324,7 @@ export function attachMenuStyler(widget: IThemable, themeService: IThemeService, return attachStyler(themeService, { ...defaultMenuStyles, ...style }, widget); } -export interface IDialogStyleOverrides extends IButtonStyleOverrides { +export interface IDialogStyleOverrides extends IButtonStyleOverrides, ILinkStyleOverrides { dialogForeground?: ColorIdentifier; dialogBackground?: ColorIdentifier; dialogShadow?: ColorIdentifier; @@ -347,6 +347,8 @@ export const defaultDialogStyles = { dialogBorder: contrastBorder, buttonForeground: buttonForeground, buttonBackground: buttonBackground, + buttonSecondaryBackground: buttonSecondaryBackground, + buttonSecondaryForeground: buttonSecondaryForeground, buttonHoverBackground: buttonHoverBackground, buttonBorder: buttonBorder, checkboxBorder: simpleCheckboxBorder, @@ -357,7 +359,8 @@ export const defaultDialogStyles = { infoIconForeground: problemsInfoIconForeground, inputBackground: inputBackground, inputForeground: inputForeground, - inputBorder: inputBorder + inputBorder: inputBorder, + textLinkForeground: textLinkForeground }; diff --git a/src/vs/platform/workspace/common/workspaceTrust.ts b/src/vs/platform/workspace/common/workspaceTrust.ts index fec04235be2..e168524439c 100644 --- a/src/vs/platform/workspace/common/workspaceTrust.ts +++ b/src/vs/platform/workspace/common/workspaceTrust.ts @@ -17,7 +17,7 @@ export function workspaceTrustToString(trustState: boolean) { if (trustState) { return localize('trusted', "Trusted"); } else { - return localize('untrusted', "Untrusted"); + return localize('untrusted', "Restricted Mode"); } } diff --git a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts index 41b06a0cea7..84c51a948ad 100644 --- a/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/browser/parts/dialogs/dialogHandler.ts @@ -113,6 +113,8 @@ export class BrowserDialogHandler implements IDialogHandler { }, renderBody, icon: customOptions?.icon, + disableCloseAction: customOptions?.disableCloseAction, + buttonDetails: customOptions?.buttonDetails, checkboxLabel: checkbox?.label, checkboxChecked: checkbox?.checked, inputs diff --git a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts index 422905ee18e..491be3401a8 100644 --- a/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts +++ b/src/vs/workbench/contrib/tasks/browser/abstractTaskService.ts @@ -80,7 +80,7 @@ import { isWorkspaceFolder, TaskQuickPickEntry, QUICKOPEN_DETAIL_CONFIG, TaskQui import { ILogService } from 'vs/platform/log/common/log'; import { once } from 'vs/base/common/functional'; import { ThemeIcon } from 'vs/platform/theme/common/themeService'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys'; const QUICKOPEN_HISTORY_LIMIT_CONFIG = 'task.quickOpen.history'; @@ -257,6 +257,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer @IPreferencesService private readonly preferencesService: IPreferencesService, @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @ILogService private readonly logService: ILogService ) { super(); @@ -931,7 +932,7 @@ export abstract class AbstractTaskService extends Disposable implements ITaskSer }).then((value) => { if (runSource === TaskRunSource.User) { this.getWorkspaceTasks().then(workspaceTasks => { - RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, this.workspaceTrustRequestService, this.openerService, workspaceTasks); + RunAutomaticTasks.promptForPermission(this, this.storageService, this.notificationService, this.workspaceTrustManagementService, this.openerService, workspaceTasks); }); } return value; diff --git a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts index 5d47d73a4ab..705dc037843 100644 --- a/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts +++ b/src/vs/workbench/contrib/tasks/browser/runAutomaticTasks.ts @@ -115,9 +115,9 @@ export class RunAutomaticTasks extends Disposable implements IWorkbenchContribut return { tasks, taskNames, locations }; } - public static async promptForPermission(taskService: ITaskService, storageService: IStorageService, notificationService: INotificationService, workspaceTrustRequestService: IWorkspaceTrustRequestService, + public static async promptForPermission(taskService: ITaskService, storageService: IStorageService, notificationService: INotificationService, workspaceTrustManagementService: IWorkspaceTrustManagementService, openerService: IOpenerService, workspaceTaskResult: Map) { - const isWorkspaceTrusted = await workspaceTrustRequestService.requestWorkspaceTrust({ modal: false }); + const isWorkspaceTrusted = workspaceTrustManagementService.isWorkpaceTrusted; if (!isWorkspaceTrusted) { return; } diff --git a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts index c02651c677e..4fa4a26bd27 100644 --- a/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts +++ b/src/vs/workbench/contrib/tasks/electron-sandbox/taskService.ts @@ -41,7 +41,7 @@ import { IPanelService } from 'vs/workbench/services/panel/common/panelService'; import { IPathService } from 'vs/workbench/services/path/common/pathService'; import { IPreferencesService } from 'vs/workbench/services/preferences/common/preferences'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; -import { IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; import { ITerminalProfileResolverService } from 'vs/workbench/contrib/terminal/common/terminal'; interface WorkspaceFolderConfigurationResult { @@ -81,6 +81,7 @@ export class TaskService extends AbstractTaskService { @IPreferencesService preferencesService: IPreferencesService, @IViewDescriptorService viewDescriptorService: IViewDescriptorService, @IWorkspaceTrustRequestService workspaceTrustRequestService: IWorkspaceTrustRequestService, + @IWorkspaceTrustManagementService workspaceTrustManagementService: IWorkspaceTrustManagementService, @ILogService logService: ILogService) { super(configurationService, markerService, @@ -111,6 +112,7 @@ export class TaskService extends AbstractTaskService { preferencesService, viewDescriptorService, workspaceTrustRequestService, + workspaceTrustManagementService, logService); this._register(lifecycleService.onBeforeShutdown(event => event.veto(this.beforeShutdown(), 'veto.tasks'))); } diff --git a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts index 4538f0945d4..59d0e9134c6 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspace.contribution.ts @@ -30,20 +30,20 @@ import { EditorInput, Extensions as EditorInputExtensions, IEditorInputSerialize import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; -import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage'; import { isWeb } from 'vs/base/common/platform'; import { IsWebContext } from 'vs/platform/contextkey/common/contextkeys'; import { dirname, resolve } from 'vs/base/common/path'; -import product from 'vs/platform/product/common/product'; -import { FileAccess } from 'vs/base/common/network'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import product from 'vs/platform/product/common/product'; import { MarkdownString } from 'vs/base/common/htmlContent'; +import { isSingleFolderWorkspaceIdentifier, toWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { Schemas } from 'vs/base/common/network'; +import { splitName } from 'vs/base/common/labels'; /* * Trust Request UX Handler */ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkbenchContribution { - private shouldShowIntroduction = true; constructor( @IDialogService private readonly dialogService: IDialogService, @@ -52,75 +52,86 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben @IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService, @IConfigurationService private readonly configurationService: IConfigurationService, @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IStorageService private readonly storageService: IStorageService, ) { super(); if (isWorkspaceTrustEnabled(configurationService)) { this.registerListeners(); - this.showIntroductionModal(); + this.showModalOnStart(); } } - private showIntroductionModal(): void { - const workspaceTrustIntroDialogDoNotShowAgainKey = 'workspace.trust.introduction.doNotShowAgain'; - const doNotShowAgain = this.storageService.getBoolean(workspaceTrustIntroDialogDoNotShowAgainKey, StorageScope.GLOBAL, false); - if (!doNotShowAgain && this.shouldShowIntroduction) { - // Show welcome dialog - (async () => { - const result = await this.dialogService.show( - Severity.Info, - localize('workspaceTrust', "Introducing Workspace Trust"), - [ - localize('manageTrust', "Manage"), - localize('close', "Close") + private async doShowModal(question: string, trustedOption: { label: string, sublabel: string }, untrustedOption: { label: string, sublabel: string }, markdownStrings: string[], trustParentString?: string): Promise { + const result = await this.dialogService.show( + Severity.Info, + question, + [ + trustedOption.label, + untrustedOption.label, + ], + { + checkbox: trustParentString ? { + label: trustParentString + } : undefined, + custom: { + buttonDetails: [ + trustedOption.sublabel, + untrustedOption.sublabel ], - { - custom: { - icon: Codicon.shield, - classes: ['workspace-trust-intro-dialog'], - markdownDetails: [ - { - markdown: new MarkdownString(localize('workspaceTrustDescription', "{0} provides many powerful features that rely on the files that are open in the current workspace. This can mean unintended code execution from the workspace and should only happen if you trust the source of the files you have open.", 'VS Code' || product.nameShort)), - }, - { - markdown: new MarkdownString(`![${localize('altTextTrustedBadge', "Shield Badge on Activity Bar")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/trusted-badge.png', require).toString(false)})\n*${localize('workspaceTrustBadgeDescription', "When features are disabled in an untrusted workspace, you will see this shield icon in the Activity Bar.")}*`), - classes: ['workspace-trust-dialog-image-row', 'badge-row'] - }, - { - markdown: new MarkdownString(`![${localize('altTextUntrustedStatus', "Workspace Trust Status Bar Entry")}](${FileAccess.asBrowserUri('vs/workbench/contrib/workspace/browser/media/untrusted-status.png', require).toString(false)})\n*${localize('workspaceTrustUntrustedDescription', "When the workspace is untrusted, you will see this status bar entry. It is hidden when the workspace is trusted.")}*`), - classes: ['workspace-trust-dialog-image-row', 'status-bar'] - }, - { - markdown: new MarkdownString(localize('seeTheDocs', "Manage your Workspace Trust configuration now to learn more or [see our docs for additional information](https://aka.ms/vscode-workspace-trust).")), - } - ], - }, - cancelId: 1, // Close - checkbox: { - label: localize('dontShowAgain', "Don't show this message again"), - checked: false, - } - } - ); - - // Dialog result - switch (result.choice) { - case 0: - this.workspaceTrustRequestService.cancelRequest(); - await this.commandService.executeCommand('workbench.trust.manage'); - break; - case 1: - this.workspaceTrustRequestService.completeRequest(undefined); - break; - } + disableCloseAction: true, + icon: Codicon.shield, + markdownDetails: markdownStrings.map(md => { return { markdown: new MarkdownString(md) }; }) + }, + } + ); + // Dialog result + switch (result.choice) { + case 0: if (result.checkboxChecked) { - this.storageService.store(workspaceTrustIntroDialogDoNotShowAgainKey, true, StorageScope.GLOBAL, StorageTarget.USER); + this.workspaceTrustManagementService.setParentFolderTrust(true); + } else { + this.workspaceTrustRequestService.completeRequest(true); } - })(); + break; + case 1: + this.workspaceTrustRequestService.cancelRequest(); + break; } + + await this.commandService.executeCommand('workbench.trust.manage'); + } + + private showModalOnStart(): void { + if (this.workspaceTrustManagementService.isWorkpaceTrusted()) { + return; + } + + let checkboxText: string | undefined; + const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceContextService.getWorkspace())!; + const isSingleFolderWorkspace = isSingleFolderWorkspaceIdentifier(workspaceIdentifier); + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + const { name } = splitName(parentPath); + checkboxText = localize('checkboxString', "I trust the author of all files in '{0}'", name); + } + + // Show Workspace Trust Start Dialog + this.doShowModal( + !isSingleFolderWorkspace ? + localize('workspaceTrust', "Do you trust the authors of the files in this workspace?") : + localize('folderTrust', "Do you trust the authors of the files in this folder?"), + { label: localize('trustOption', "Yes, I trust the authors"), sublabel: isSingleFolderWorkspace ? localize('trustFolderOptionDescription', "Trust folder and enable all features") : localize('trustWorkspaceOptionDescription', "Trust workspace and enable all features") }, + { label: localize('dontTrustOption', "No, I don't trust the authors"), sublabel: isSingleFolderWorkspace ? localize('dontTrustFolderOptionDescription', "Browse folder in restricted mode") : localize('dontTrustWorkspaceOptionDescription', "Browse workspace in restricted mode") }, + [ + !isSingleFolderWorkspace ? + localize('workspaceStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this workspace.", product.nameShort) : + localize('folderStartupTrustDetails', "{0} provides advanced editing features that may automatically execute files in this folder.", product.nameShort), + localize('learnMore', "If you don't trust the author of these files we recommend to continue in restricted mode as the files may be malicious. See [our docs](https://aka.ms/vscode-workspace-trust) to learn more.") + ], + checkboxText + ); } private registerListeners(): void { @@ -206,9 +217,6 @@ export class WorkspaceTrustRequestHandler extends Disposable implements IWorkben resolve(); })); })); - - // Don't auto-show the UX editor if the request is 5 seconds after startup - setTimeout(() => { this.shouldShowIntroduction = false; }, 5000); } } diff --git a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts index 089bc7b8a4f..ad344191ada 100644 --- a/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts +++ b/src/vs/workbench/contrib/workspace/browser/workspaceTrustEditor.ts @@ -26,6 +26,7 @@ import { ExtensionUntrustedWorkpaceSupportType } from 'vs/platform/extensions/co import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IPromptChoiceWithMenu, Severity } from 'vs/platform/notification/common/notification'; import { Link } from 'vs/platform/opener/browser/link'; +import product from 'vs/platform/product/common/product'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { foreground } from 'vs/platform/theme/common/colorRegistry'; @@ -154,7 +155,7 @@ export class WorkspaceTrustEditor extends EditorPane { return localize('trustedDescription', "All features are enabled because trust has been granted to the workspace. [Learn more](https://aka.ms/vscode-workspace-trust)."); } - return localize('untrustedDescription', "Some features are disabled until trust is granted to the workspace. [Learn more](https://aka.ms/vscode-workspace-trust)."); + return localize('untrustedDescription', "{0} is in a restricted mode optimized for browsing code safely. [Learn more](https://aka.ms/vscode-workspace-trust).", product.nameShort); } private getHeaderTitleIconClassNames(trusted: boolean): string[] { @@ -271,7 +272,7 @@ export class WorkspaceTrustEditor extends EditorPane { ], checkListIcon.classNamesArray); const untrustedContainer = append(this.affectedFeaturesContainer, $('.workspace-trust-limitations.untrusted')); - this.renderLimitationsHeaderElement(untrustedContainer, localize('untrustedWorkspace', "Untrusted Workspace"), this.getHeaderTitleIconClassNames(false)); + this.renderLimitationsHeaderElement(untrustedContainer, localize('untrustedWorkspace', "Restricted Mode"), this.getHeaderTitleIconClassNames(false)); this.renderLimitationsListElement(untrustedContainer, [ localize('untrustedTasks', "Tasks will be disabled"), diff --git a/src/vs/workbench/services/configuration/browser/configurationService.ts b/src/vs/workbench/services/configuration/browser/configurationService.ts index 688fb91c4ad..581939f03c1 100644 --- a/src/vs/workbench/services/configuration/browser/configurationService.ts +++ b/src/vs/workbench/services/configuration/browser/configurationService.ts @@ -14,7 +14,7 @@ import { IWorkspaceContextService, Workspace as BaseWorkspace, WorkbenchState, I import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent, AllKeysConfigurationChangeEvent, mergeChanges } from 'vs/platform/configuration/common/configurationModels'; import { IConfigurationChangeEvent, ConfigurationTarget, IConfigurationOverrides, keyFromOverrideIdentifier, isConfigurationOverrides, IConfigurationData, IConfigurationValue, IConfigurationChange, ConfigurationTargetToString } from 'vs/platform/configuration/common/configuration'; import { Configuration } from 'vs/workbench/services/configuration/common/configurationModels'; -import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, UntrustedSettings, filterSettingsRequireWorkspaceTrust } from 'vs/workbench/services/configuration/common/configuration'; +import { FOLDER_CONFIG_FOLDER_NAME, defaultSettingsSchemaId, userSettingsSchemaId, workspaceSettingsSchemaId, folderSettingsSchemaId, IConfigurationCache, machineSettingsSchemaId, LOCAL_MACHINE_SCOPES, IWorkbenchConfigurationService, UntrustedSettings } from 'vs/workbench/services/configuration/common/configuration'; import { Registry } from 'vs/platform/registry/common/platform'; import { IConfigurationRegistry, Extensions, allSettings, windowSettings, resourceSettings, applicationSettings, machineSettings, machineOverridableSettings, ConfigurationScope, IConfigurationPropertySchema } from 'vs/platform/configuration/common/configurationRegistry'; import { IWorkspaceIdentifier, isWorkspaceIdentifier, IStoredWorkspaceFolder, isStoredWorkspaceFolder, IWorkspaceFolderCreationData, IWorkspaceInitializationPayload, IEmptyWorkspaceIdentifier, useSlashForPath, getStoredWorkspaceFolder, isSingleFolderWorkspaceIdentifier, ISingleFolderWorkspaceIdentifier, toWorkspaceFolders } from 'vs/platform/workspaces/common/workspaces'; @@ -32,7 +32,7 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle import { ILogService } from 'vs/platform/log/common/log'; import { toErrorMessage } from 'vs/base/common/errorMessage'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; -import { IWorkspaceTrustManagementService, IWorkspaceTrustRequestService } from 'vs/platform/workspace/common/workspaceTrust'; +import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust'; import { delta, distinct } from 'vs/base/common/arrays'; import { forEach, IStringDictionary } from 'vs/base/common/collections'; @@ -955,24 +955,6 @@ export class WorkspaceService extends Disposable implements IWorkbenchConfigurat } } -class ConfigurationWorkspaceTrustContribution extends Disposable implements IWorkbenchContribution { - constructor( - @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, - @IWorkbenchConfigurationService private readonly configurationService: IWorkbenchConfigurationService - ) { - super(); - this.requestTrust(); - this._register(configurationService.onDidChangeUntrustdSettings(() => this.requestTrust())); - } - - private requestTrust(): void { - const settingsRequiringWorkspaceTrust = filterSettingsRequireWorkspaceTrust(this.configurationService.unTrustedSettings.default); - if (settingsRequiringWorkspaceTrust.length) { - this.workspaceTrustRequestService.requestWorkspaceTrust(); - } - } -} - class RegisterConfigurationSchemasContribution extends Disposable implements IWorkbenchContribution { constructor( @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @@ -1095,4 +1077,3 @@ class RegisterConfigurationSchemasContribution extends Disposable implements IWo const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); workbenchContributionsRegistry.registerWorkbenchContribution(RegisterConfigurationSchemasContribution, LifecyclePhase.Restored); -workbenchContributionsRegistry.registerWorkbenchContribution(ConfigurationWorkspaceTrustContribution, LifecyclePhase.Starting); diff --git a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts index 6a0dab54858..125e552dacf 100644 --- a/src/vs/workbench/services/workspaces/common/workspaceTrust.ts +++ b/src/vs/workbench/services/workspaces/common/workspaceTrust.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter } from 'vs/base/common/event'; +import { splitName } from 'vs/base/common/labels'; import { Disposable } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { dirname } from 'vs/base/common/resources'; @@ -212,6 +213,12 @@ export class WorkspaceTrustManagementService extends Disposable implements IWork } setParentFolderTrust(trusted: boolean): void { + const workspaceIdentifier = toWorkspaceIdentifier(this.workspaceService.getWorkspace()); + if (isSingleFolderWorkspaceIdentifier(workspaceIdentifier) && workspaceIdentifier.uri.scheme === Schemas.file) { + const { parentPath } = splitName(workspaceIdentifier.uri.fsPath); + + this.setFoldersTrust([URI.file(parentPath)], trusted); + } } setWorkspaceTrust(trusted: boolean): void {