From 3625834028767b0153acd7962801d0fe52e02bf2 Mon Sep 17 00:00:00 2001 From: Anthony Dresser Date: Thu, 25 Apr 2019 19:18:04 -0700 Subject: [PATCH] Merge from vscode 63655183ba5305b70ffaf1327b8a4708f0a79bd9 (#5221) --- src/vs/platform/environment/node/argv.ts | 2 +- .../api/common/menusExtensionPoint.ts | 1 + .../electron-browser/extensionsWidgets.ts | 7 +- .../media/extensionsViewlet.css | 8 +- .../contrib/files/browser/fileActions.ts | 12 +-- .../files/browser/views/explorerViewer.ts | 11 ++- .../contrib/files/common/explorerService.ts | 2 +- .../dialogs/browser/remoteFileDialog.ts | 80 +++++++++++++------ .../workspaceEditingService.ts | 4 +- 9 files changed, 73 insertions(+), 54 deletions(-) diff --git a/src/vs/platform/environment/node/argv.ts b/src/vs/platform/environment/node/argv.ts index 478e1f220d..97e7efb1bc 100644 --- a/src/vs/platform/environment/node/argv.ts +++ b/src/vs/platform/environment/node/argv.ts @@ -47,7 +47,7 @@ export const options: Option[] = [ { id: 'extensions-dir', type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") }, { id: 'list-extensions', type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") }, { id: 'show-versions', type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") }, - { id: 'install-extension', type: 'string', cat: 'e', args: 'extension-id', description: localize('installExtension', "Installs or updates the extension. Use `--force` argument to avoid prompts.") }, + { id: 'install-extension', type: 'string', cat: 'e', args: 'extension-id | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. Use `--force` argument to avoid prompts.") }, { id: 'uninstall-extension', type: 'string', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") }, { id: 'enable-proposed-api', type: 'string', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") }, diff --git a/src/vs/workbench/api/common/menusExtensionPoint.ts b/src/vs/workbench/api/common/menusExtensionPoint.ts index a1cb49b8d9..7791d9ae48 100644 --- a/src/vs/workbench/api/common/menusExtensionPoint.ts +++ b/src/vs/workbench/api/common/menusExtensionPoint.ts @@ -34,6 +34,7 @@ namespace schema { case 'explorer/context': return MenuId.ExplorerContext; case 'editor/title/context': return MenuId.EditorTitleContext; case 'debug/callstack/context': return MenuId.DebugCallStackContext; + case 'debug/toolbar': return MenuId.DebugToolBar; case 'debug/toolBar': return MenuId.DebugToolBar; case 'menuBar/file': return MenuId.MenubarFileMenu; case 'scm/title': return MenuId.SCMTitle; diff --git a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts index 6228de3bd3..039975b901 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts +++ b/src/vs/workbench/contrib/extensions/electron-browser/extensionsWidgets.ts @@ -13,8 +13,7 @@ import { IExtensionManagementServerService, IExtensionTipsService } from 'vs/pla import { ILabelService } from 'vs/platform/label/common/label'; import { extensionButtonProminentBackground, extensionButtonProminentForeground, DisabledLabelAction, ReloadAction } from 'vs/workbench/contrib/extensions/electron-browser/extensionsActions'; import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService'; -import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_FOREGROUND } from 'vs/workbench/common/theme'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { STATUS_BAR_HOST_NAME_BACKGROUND, STATUS_BAR_HOST_NAME_FOREGROUND } from 'vs/workbench/common/theme'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService'; import { Emitter, Event } from 'vs/base/common/event'; @@ -310,7 +309,6 @@ class RemoteBadge extends Disposable { private readonly tooltip: boolean, @ILabelService private readonly labelService: ILabelService, @IThemeService private readonly themeService: IThemeService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService ) { super(); @@ -326,13 +324,12 @@ class RemoteBadge extends Disposable { return; } const bgColor = this.themeService.getTheme().getColor(STATUS_BAR_HOST_NAME_BACKGROUND); - const fgColor = this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY ? this.themeService.getTheme().getColor(STATUS_BAR_NO_FOLDER_FOREGROUND) : this.themeService.getTheme().getColor(STATUS_BAR_FOREGROUND); + const fgColor = this.themeService.getTheme().getColor(STATUS_BAR_HOST_NAME_FOREGROUND); this.element.style.backgroundColor = bgColor ? bgColor.toString() : ''; this.element.style.color = fgColor ? fgColor.toString() : ''; }; applyBadgeStyle(); this._register(this.themeService.onThemeChange(() => applyBadgeStyle())); - this._register(this.workspaceContextService.onDidChangeWorkbenchState(() => applyBadgeStyle())); if (this.tooltip) { const updateTitle = () => { diff --git a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css index d21056d85d..38d72359d3 100644 --- a/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css +++ b/src/vs/workbench/contrib/extensions/electron-browser/media/extensionsViewlet.css @@ -110,11 +110,9 @@ height: 22px; line-height: 22px; border-radius: 20px; - text-align: center; -} - -.extensions-viewlet > .extensions .monaco-list-row > .extension > .icon-container .extension-remote-badge > .octicon { - vertical-align: middle + display: flex; + align-items: center; + justify-content: center; } .extensions-viewlet > .extensions .monaco-list-row > .extension > .details > .header-container > .header > .extension-remote-badge-container { diff --git a/src/vs/workbench/contrib/files/browser/fileActions.ts b/src/vs/workbench/contrib/files/browser/fileActions.ts index 758ca2fae2..028b62f98c 100644 --- a/src/vs/workbench/contrib/files/browser/fileActions.ts +++ b/src/vs/workbench/contrib/files/browser/fileActions.ts @@ -166,7 +166,7 @@ export class GlobalNewUntitledFileAction extends Action { } } -function deleteFiles(serviceAccesor: ServicesAccessor, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { +function deleteFiles(textFileService: ITextFileService, dialogService: IDialogService, configurationService: IConfigurationService, fileService: IFileService, elements: ExplorerItem[], useTrash: boolean, skipConfirm = false): Promise { let primaryButton: string; if (useTrash) { primaryButton = isWindows ? nls.localize('deleteButtonLabelRecycleBin', "&&Move to Recycle Bin") : nls.localize({ key: 'deleteButtonLabelTrash', comment: ['&& denotes a mnemonic'] }, "&&Move to Trash"); @@ -175,10 +175,6 @@ function deleteFiles(serviceAccesor: ServicesAccessor, elements: ExplorerItem[], } const distinctElements = resources.distinctParents(elements, e => e.resource); - const textFileService = serviceAccesor.get(ITextFileService); - const dialogService = serviceAccesor.get(IDialogService); - const configurationService = serviceAccesor.get(IConfigurationService); - const fileService = serviceAccesor.get(IFileService); // Handle dirty let confirmDirtyPromise: Promise = Promise.resolve(true); @@ -296,7 +292,7 @@ function deleteFiles(serviceAccesor: ServicesAccessor, elements: ExplorerItem[], skipConfirm = true; - return deleteFiles(serviceAccesor, elements, useTrash, skipConfirm); + return deleteFiles(textFileService, dialogService, configurationService, fileService, elements, useTrash, skipConfirm); } return Promise.resolve(); @@ -1011,7 +1007,7 @@ export const moveFileToTrashHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; - return deleteFiles(accessor, stats, true); + return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, true); }; export const deleteFileHandler = (accessor: ServicesAccessor) => { @@ -1022,7 +1018,7 @@ export const deleteFileHandler = (accessor: ServicesAccessor) => { const explorerContext = getContext(listService.lastFocusedList); const stats = explorerContext.selection.length > 1 ? explorerContext.selection : [explorerContext.stat!]; - return deleteFiles(accessor, stats, false); + return deleteFiles(accessor.get(ITextFileService), accessor.get(IDialogService), accessor.get(IConfigurationService), accessor.get(IFileService), stats, false); }; let pasteShouldMove = false; diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 11dce2e80b..fdad5d3b44 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -474,12 +474,6 @@ export class FileDragAndDrop implements ITreeDragAndDrop { if (typesArray.indexOf(DataTransfers.FILES.toLowerCase()) === -1 && typesArray.indexOf(CodeDataTransfers.FILES.toLowerCase()) === -1) { return false; } - if (this.environmentService.configuration.remoteAuthority) { - const resources = extractResources(originalEvent, true); - if (resources.some(r => r.resource.authority !== this.environmentService.configuration.remoteAuthority)) { - return false; - } - } } // Other-Tree DND @@ -611,6 +605,11 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private handleExternalDrop(data: DesktopDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { const droppedResources = extractResources(originalEvent, true); + if (this.environmentService.configuration.remoteAuthority) { + if (droppedResources.some(r => r.resource.authority !== this.environmentService.configuration.remoteAuthority)) { + return Promise.resolve(); + } + } // Check for dropped external files to be folders return this.fileService.resolveAll(droppedResources).then(result => { diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index ad8e439982..45da0cdab0 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -155,7 +155,7 @@ export class ExplorerService implements IExplorerService { } // Stat needs to be resolved first and then revealed - const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: false }; + const options: IResolveFileOptions = { resolveTo: [resource], resolveMetadata: this.sortOrder === 'modified' }; const workspaceFolder = this.contextService.getWorkspaceFolder(resource); const rootUri = workspaceFolder ? workspaceFolder.uri : this.roots[0].resource; const root = this.roots.filter(r => r.resource.toString() === rootUri.toString()).pop()!; diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index a16e424f52..319f152ccf 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -9,7 +9,7 @@ import * as objects from 'vs/base/common/objects'; import { IFileService, IFileStat, FileKind } from 'vs/platform/files/common/files'; import { IQuickInputService, IQuickPickItem, IQuickPick } from 'vs/platform/quickinput/common/quickInput'; import { URI } from 'vs/base/common/uri'; -import { isWindows } from 'vs/base/common/platform'; +import { isWindows, OperatingSystem } from 'vs/base/common/platform'; import { ISaveDialogOptions, IOpenDialogOptions, IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { ILabelService } from 'vs/platform/label/common/label'; @@ -26,6 +26,7 @@ import { RemoteFileDialogContext } from 'vs/workbench/common/contextkeys'; import { equalsIgnoreCase, format } from 'vs/base/common/strings'; import { OpenLocalFileAction, OpenLocalFileFolderAction, OpenLocalFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { IRemoteAgentEnvironment } from 'vs/platform/remote/common/remoteAgentEnvironment'; interface FileQuickPickItem extends IQuickPickItem { uri: URI; @@ -40,7 +41,8 @@ enum UpdateResult { } // Reference: https://en.wikipedia.org/wiki/Filename -const INVALID_FILE_CHARS = isWindows ? /[\\/:\*\?"<>\|]/g : /[\\/]/g; +const WINDOWS_INVALID_FILE_CHARS = /[\\/:\*\?"<>\|]/g; +const UNIX_INVALID_FILE_CHARS = /[\\/]/g; const WINDOWS_FORBIDDEN_NAMES = /^(con|prn|aux|clock\$|nul|lpt[0-9]|com[0-9])$/i; export class RemoteFileDialog { @@ -60,6 +62,7 @@ export class RemoteFileDialog { private activeItem: FileQuickPickItem; private userHome: URI; private badPath: string | undefined; + private remoteAgentEnvironment: IRemoteAgentEnvironment | null; constructor( @IFileService private readonly fileService: IFileService, @@ -136,9 +139,16 @@ export class RemoteFileDialog { return defaultUri ? defaultUri.scheme : (available ? available[0] : Schemas.file); } + private async getRemoteAgentEnvironment(): Promise { + if (this.remoteAgentEnvironment === undefined) { + this.remoteAgentEnvironment = await this.remoteAgentService.getEnvironment(); + } + return this.remoteAgentEnvironment; + } + private async getUserHome(): Promise { if (this.scheme !== Schemas.file) { - const env = await this.remoteAgentService.getEnvironment(); + const env = await this.getRemoteAgentEnvironment(); if (env) { return env.userHome; } @@ -291,7 +301,7 @@ export class RemoteFileDialog { this.filePickBox.show(); this.contextKey.set(true); - await this.updateItems(homedir, this.trailing); + await this.updateItems(homedir, false, this.trailing); if (this.trailing) { this.filePickBox.valueSelection = [this.filePickBox.value.length - this.trailing.length, this.filePickBox.value.length - ext.length]; } else { @@ -361,7 +371,7 @@ export class RemoteFileDialog { } } else if (navigateValue) { // Try to navigate into the folder. - await this.updateItems(navigateValue, this.trailing); + await this.updateItems(navigateValue, true, this.trailing); } else { // validation error. Path does not exist. } @@ -373,8 +383,12 @@ export class RemoteFileDialog { if (this.filePickBox.busy) { this.badPath = undefined; return UpdateResult.Updating; - } else if (value[value.length - 1] === '~') { - await this.updateItems(this.userHome); + } else if ((value.length > 0) && ((value[value.length - 1] === '~') || (value[0] === '~'))) { + let newDir = this.userHome; + if ((value[0] === '~') && (value.length > 1)) { + newDir = resources.joinPath(newDir, value.substring(1)); + } + await this.updateItems(newDir, true); return UpdateResult.Updated; } else if (this.endsWithSlash(value) || (!resources.isEqual(this.currentFolder, resources.dirname(valueUri), true) && resources.isEqualOrParent(this.currentFolder, resources.dirname(valueUri), true))) { let stat: IFileStat | undefined; @@ -403,7 +417,7 @@ export class RemoteFileDialog { // do nothing } if (statWithoutTrailing && statWithoutTrailing.isDirectory && (resources.basename(valueUri) !== '.')) { - await this.updateItems(inputUriDirname, resources.basename(valueUri)); + await this.updateItems(inputUriDirname, false, resources.basename(valueUri)); this.badPath = undefined; return UpdateResult.Updated; } @@ -501,18 +515,16 @@ export class RemoteFileDialog { // Make sure that the suffix is added. If the user deleted it, we automatically add it here let hasExt: boolean = false; const currentExt = resources.extname(uri).substr(1); - if (currentExt !== '') { - for (let i = 0; i < this.options.filters.length; i++) { - for (let j = 0; j < this.options.filters[i].extensions.length; j++) { - if ((this.options.filters[i].extensions[j] === '*') || (this.options.filters[i].extensions[j] === currentExt)) { - hasExt = true; - break; - } - } - if (hasExt) { + for (let i = 0; i < this.options.filters.length; i++) { + for (let j = 0; j < this.options.filters[i].extensions.length; j++) { + if ((this.options.filters[i].extensions[j] === '*') || (this.options.filters[i].extensions[j] === currentExt)) { + hasExt = true; break; } } + if (hasExt) { + break; + } } if (!hasExt) { result = resources.joinPath(resources.dirname(uri), resources.basename(uri) + '.' + this.options.filters[0].extensions[0]); @@ -583,7 +595,7 @@ export class RemoteFileDialog { // Show a yes/no prompt const message = nls.localize('remoteFileDialog.validateExisting', '{0} already exists. Are you sure you want to overwrite it?', resources.basename(uri)); return this.yesNoPrompt(uri, message); - } else if (!this.isValidBaseName(resources.basename(uri))) { + } else if (!(await this.isValidBaseName(resources.basename(uri)))) { // Filename not allowed this.filePickBox.validationMessage = nls.localize('remoteFileDialog.validateBadFilename', 'Please enter a valid file name.'); return Promise.resolve(false); @@ -610,7 +622,7 @@ export class RemoteFileDialog { return Promise.resolve(true); } - private async updateItems(newFolder: URI, trailing?: string) { + private async updateItems(newFolder: URI, force: boolean = false, trailing?: string) { this.filePickBox.busy = true; this.userEnteredPathSegment = trailing ? trailing : ''; this.autoCompletePathSegment = ''; @@ -628,13 +640,18 @@ export class RemoteFileDialog { if (!equalsIgnoreCase(this.filePickBox.value.substring(0, newValue.length), newValue)) { this.filePickBox.valueSelection = [0, this.filePickBox.value.length]; this.insertText(newValue, newValue); - } else if (equalsIgnoreCase(this.pathFromUri(resources.dirname(oldFolder), true), newFolderPath)) { - // This is the case where the user went up one dir. We need to make sure that we remove the final dir. + } else if (force || equalsIgnoreCase(this.pathFromUri(resources.dirname(oldFolder), true), newFolderPath)) { + // This is the case where the user went up one dir or is clicking on dirs. We need to make sure that we remove the final dir. this.filePickBox.valueSelection = [newFolderPath.length, this.filePickBox.value.length]; this.insertText(newValue, ''); } } - this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; + if (force && trailing) { + // Keep the cursor position in front of the save as name. + this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - trailing.length]; + } else { + this.filePickBox.valueSelection = [this.filePickBox.value.length, this.filePickBox.value.length]; + } this.filePickBox.busy = false; }); } @@ -662,17 +679,28 @@ export class RemoteFileDialog { } } - private isValidBaseName(name: string): boolean { + private async isWindowsOS(): Promise { + let isWindowsOS = isWindows; + const env = await this.getRemoteAgentEnvironment(); + if (env) { + isWindowsOS = env.os === OperatingSystem.Windows; + } + return isWindowsOS; + } + + private async isValidBaseName(name: string): Promise { if (!name || name.length === 0 || /^\s+$/.test(name)) { return false; // require a name that is not just whitespace } + const isWindowsOS = await this.isWindowsOS(); + const INVALID_FILE_CHARS = isWindowsOS ? WINDOWS_INVALID_FILE_CHARS : UNIX_INVALID_FILE_CHARS; INVALID_FILE_CHARS.lastIndex = 0; // the holy grail of software development if (INVALID_FILE_CHARS.test(name)) { return false; // check for certain invalid file characters } - if (isWindows && WINDOWS_FORBIDDEN_NAMES.test(name)) { + if (isWindowsOS && WINDOWS_FORBIDDEN_NAMES.test(name)) { return false; // check for certain invalid file names } @@ -680,11 +708,11 @@ export class RemoteFileDialog { return false; // check for reserved values } - if (isWindows && name[name.length - 1] === '.') { + if (isWindowsOS && name[name.length - 1] === '.') { return false; // Windows: file cannot end with a "." } - if (isWindows && name.length !== name.trim().length) { + if (isWindowsOS && name.length !== name.trim().length) { return false; // Windows: file cannot end with a whitespace } diff --git a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts index 30266ddfe3..741cfa8884 100644 --- a/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/electron-browser/workspaceEditingService.ts @@ -244,7 +244,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } async createAndEnterWorkspace(folders: IWorkspaceFolderCreationData[], path?: URI): Promise { - if (path && !this.isValidTargetWorkspacePath(path)) { + if (path && !await this.isValidTargetWorkspacePath(path)) { return Promise.reject(null); } const remoteAuthority = this.environmentService.configuration.remoteAuthority; @@ -258,7 +258,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { } async saveAndEnterWorkspace(path: URI): Promise { - if (!this.isValidTargetWorkspacePath(path)) { + if (!await this.isValidTargetWorkspacePath(path)) { return Promise.reject(null); } const workspaceIdentifier = this.getCurrentWorkspaceIdentifier();