diff --git a/build/azure-pipelines/linux/snap-build-linux.yml b/build/azure-pipelines/linux/snap-build-linux.yml index 928ff84e06..7d3f5fefc1 100644 --- a/build/azure-pipelines/linux/snap-build-linux.yml +++ b/build/azure-pipelines/linux/snap-build-linux.yml @@ -16,6 +16,9 @@ steps: - script: | set -e + # Get snapcraft version + snapcraft --version + # Make sure we get latest packages sudo apt-get update sudo apt-get upgrade -y @@ -38,7 +41,7 @@ steps: PACKAGEJSON="$(ls $SNAP_ROOT/code*/usr/share/code*/resources/app/package.json)" VERSION=$(node -p "require(\"$PACKAGEJSON\").version") SNAP_PATH="$SNAP_ROOT/$SNAP_FILENAME" - (cd $SNAP_ROOT/code-* && snapcraft snap --output "$SNAP_PATH") + (cd $SNAP_ROOT/code-* && sudo snapcraft snap --output "$SNAP_PATH") # Publish snap package AZURE_DOCUMENTDB_MASTERKEY="$(AZURE_DOCUMENTDB_MASTERKEY)" \ diff --git a/build/gulpfile.vscode.linux.js b/build/gulpfile.vscode.linux.js index 8ffb63596c..ed72b6b355 100644 --- a/build/gulpfile.vscode.linux.js +++ b/build/gulpfile.vscode.linux.js @@ -203,11 +203,17 @@ function prepareSnapPackage(arch) { return function () { const desktop = gulp.src('resources/linux/code.desktop', { base: '.' }) + .pipe(rename(`usr/share/applications/${product.applicationName}.desktop`)); + + const desktopUrlHandler = gulp.src('resources/linux/code-url-handler.desktop', { base: '.' }) + .pipe(rename(`usr/share/applications/${product.applicationName}-url-handler.desktop`)); + + const desktops = es.merge(desktop, desktopUrlHandler) .pipe(replace('@@NAME_LONG@@', product.nameLong)) .pipe(replace('@@NAME_SHORT@@', product.nameShort)) .pipe(replace('@@NAME@@', product.applicationName)) .pipe(replace('@@ICON@@', `/usr/share/pixmaps/${product.linuxIconName}.png`)) - .pipe(rename(`usr/share/applications/${product.applicationName}.desktop`)); + .pipe(replace('@@URLPROTOCOL@@', product.urlProtocol)); const icon = gulp.src('resources/linux/code.png', { base: '.' }) .pipe(rename(`usr/share/pixmaps/${product.linuxIconName}.png`)); @@ -223,7 +229,7 @@ function prepareSnapPackage(arch) { const electronLaunch = gulp.src('resources/linux/snap/electron-launch', { base: '.' }) .pipe(rename('electron-launch')); - const all = es.merge(desktop, icon, code, snapcraft, electronLaunch); + const all = es.merge(desktops, icon, code, snapcraft, electronLaunch); return all.pipe(vfs.dest(destination)); }; diff --git a/src/vs/platform/dialogs/common/dialogs.ts b/src/vs/platform/dialogs/common/dialogs.ts index c19c583d31..d210a1dcee 100644 --- a/src/vs/platform/dialogs/common/dialogs.ts +++ b/src/vs/platform/dialogs/common/dialogs.ts @@ -45,6 +45,7 @@ export interface IPickAndOpenOptions { forceNewWindow?: boolean; defaultUri?: URI; telemetryExtraData?: ITelemetryData; + availableFileSystems?: string[]; } export interface ISaveDialogOptions { diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index ba2830b40c..8c281da83e 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -285,30 +285,31 @@ export enum FileSystemProviderErrorCode { FileNotADirectory = 'EntryNotADirectory', FileIsADirectory = 'EntryIsADirectory', NoPermissions = 'NoPermissions', - Unavailable = 'Unavailable' + Unavailable = 'Unavailable', + Unknown = 'Unknown' } export class FileSystemProviderError extends Error { - constructor(message: string, public readonly code?: FileSystemProviderErrorCode) { + constructor(message: string, public readonly code: FileSystemProviderErrorCode) { super(message); } } -export function createFileSystemProviderError(error: Error, code?: FileSystemProviderErrorCode): FileSystemProviderError { +export function createFileSystemProviderError(error: Error, code: FileSystemProviderErrorCode): FileSystemProviderError { const providerError = new FileSystemProviderError(error.toString(), code); - markAsFileSystemProviderError(providerError); + markAsFileSystemProviderError(providerError, code); return providerError; } -export function markAsFileSystemProviderError(error: Error, code?: FileSystemProviderErrorCode): Error { +export function markAsFileSystemProviderError(error: Error, code: FileSystemProviderErrorCode): Error { error.name = code ? `${code} (FileSystemError)` : `FileSystemError`; return error; } -export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderErrorCode | undefined { +export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderErrorCode { // FileSystemProviderError comes with the code if (error instanceof FileSystemProviderError) { @@ -319,7 +320,7 @@ export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderE // went through the markAsFileSystemProviderError() method const match = /^(.+) \(FileSystemError\)$/.exec(error.name); if (!match) { - return undefined; + return FileSystemProviderErrorCode.Unknown; } switch (match[1]) { @@ -331,7 +332,7 @@ export function toFileSystemProviderErrorCode(error: Error): FileSystemProviderE case FileSystemProviderErrorCode.Unavailable: return FileSystemProviderErrorCode.Unavailable; } - return undefined; + return FileSystemProviderErrorCode.Unknown; } export function toFileOperationResult(error: Error): FileOperationResult { diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 7cd5f2ea18..ab2cc73669 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -6599,10 +6599,11 @@ declare module 'vscode' { * * @param name Optional human-readable string which will be used to represent the terminal in the UI. * @param shellPath Optional path to a custom shell executable to be used in the terminal. - * @param shellArgs Optional args for the custom shell executable, this does not work on Windows (see #8429) + * @param shellArgs Optional args for the custom shell executable. A string can be used on Windows only which + * allows specifying shell args in [command-line format](https://msdn.microsoft.com/en-au/08dfcab2-eb6e-49a4-80eb-87d4076c98c6). * @return A new Terminal. */ - export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): Terminal; + export function createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): Terminal; /** * Creates a [Terminal](#Terminal). The cwd of the terminal will be the workspace directory diff --git a/src/vs/workbench/api/common/configurationExtensionPoint.ts b/src/vs/workbench/api/common/configurationExtensionPoint.ts index 6138239057..36cbbc3aea 100644 --- a/src/vs/workbench/api/common/configurationExtensionPoint.ts +++ b/src/vs/workbench/api/common/configurationExtensionPoint.ts @@ -294,6 +294,9 @@ jsonRegistry.registerSchema('vscode://schemas/workspaceConfig', { default: {}, description: nls.localize('workspaceConfig.extensions.description', "Workspace extensions"), $ref: 'vscode://schemas/extensions' + }, + 'remoteAuthority': { + type: 'string' } }, additionalProperties: false, diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index 0a2e6dd73e..a2bd8e3e89 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -479,7 +479,7 @@ export function createApiFactory( createWebviewPanel(viewType: string, title: string, showOptions: vscode.ViewColumn | { viewColumn: vscode.ViewColumn, preserveFocus?: boolean }, options: vscode.WebviewPanelOptions & vscode.WebviewOptions): vscode.WebviewPanel { return extHostWebviews.createWebviewPanel(extension, viewType, title, showOptions, options); }, - createTerminal(nameOrOptions?: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + createTerminal(nameOrOptions?: vscode.TerminalOptions | string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { return extHostTerminalService.createTerminalFromOptions(nameOrOptions); } diff --git a/src/vs/workbench/api/node/extHostCommands.ts b/src/vs/workbench/api/node/extHostCommands.ts index 0f6f52315e..36583389cd 100644 --- a/src/vs/workbench/api/node/extHostCommands.ts +++ b/src/vs/workbench/api/node/extHostCommands.ts @@ -148,7 +148,7 @@ 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}`)); + return Promise.reject(new Error(`Running the contributed command: '${id}' failed. Illegal argument '${description.args[i].name}' - ${description.args[i].description}`)); } } } @@ -158,7 +158,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { return Promise.resolve(result); } catch (err) { this._logService.error(err, id); - return Promise.reject(new Error(`Running the contributed command:'${id}' failed.`)); + return Promise.reject(new Error(`Running the contributed command: '${id}' failed.`)); } } diff --git a/src/vs/workbench/api/node/extHostComments.ts b/src/vs/workbench/api/node/extHostComments.ts index ba4325f930..36aee64975 100644 --- a/src/vs/workbench/api/node/extHostComments.ts +++ b/src/vs/workbench/api/node/extHostComments.ts @@ -651,7 +651,7 @@ function convertToModeComment(commentController: ExtHostCommentController, vscod isDraft: vscodeComment.isDraft, selectCommand: vscodeComment.selectCommand ? commandsConverter.toInternal(vscodeComment.selectCommand) : undefined, editCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.editCommand) : undefined, - deleteCommand: vscodeComment.editCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined, + deleteCommand: vscodeComment.deleteCommand ? commandsConverter.toInternal(vscodeComment.deleteCommand) : undefined, label: vscodeComment.label, commentReactions: vscodeComment.commentReactions ? vscodeComment.commentReactions.map(reaction => convertToReaction2(commentController.reactionProvider, reaction)) : undefined }; diff --git a/src/vs/workbench/api/node/extHostTerminalService.ts b/src/vs/workbench/api/node/extHostTerminalService.ts index 88a9984867..9236f8238d 100644 --- a/src/vs/workbench/api/node/extHostTerminalService.ts +++ b/src/vs/workbench/api/node/extHostTerminalService.ts @@ -293,7 +293,7 @@ export class ExtHostTerminalService implements ExtHostTerminalServiceShape { this._proxy = mainContext.getProxy(MainContext.MainThreadTerminalService); } - public createTerminal(name?: string, shellPath?: string, shellArgs?: string[]): vscode.Terminal { + public createTerminal(name?: string, shellPath?: string, shellArgs?: string[] | string): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, name); terminal.create(shellPath, shellArgs); this._terminals.push(terminal); diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index 1bfafb8109..b502bd07a6 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -2207,12 +2207,12 @@ export class FileSystemError extends Error { return new FileSystemError(messageOrUri, FileSystemProviderErrorCode.Unavailable, FileSystemError.Unavailable); } - constructor(uriOrMessage?: string | URI, code?: string, terminator?: Function) { + constructor(uriOrMessage?: string | URI, code: FileSystemProviderErrorCode = FileSystemProviderErrorCode.Unknown, terminator?: Function) { super(URI.isUri(uriOrMessage) ? uriOrMessage.toString(true) : uriOrMessage); // mark the error as file system provider error so that // we can extract the error code on the receiving side - markAsFileSystemProviderError(this); + markAsFileSystemProviderError(this, code); // workaround when extending builtin objects and when compiling to ES5, see: // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work diff --git a/src/vs/workbench/browser/actions/workspaceActions.ts b/src/vs/workbench/browser/actions/workspaceActions.ts index 351b3027cc..2f14866c85 100644 --- a/src/vs/workbench/browser/actions/workspaceActions.ts +++ b/src/vs/workbench/browser/actions/workspaceActions.ts @@ -15,6 +15,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands'; import { ADD_ROOT_FOLDER_COMMAND_ID, ADD_ROOT_FOLDER_LABEL, PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { Schemas } from 'vs/base/common/network'; export class OpenFileAction extends Action { @@ -34,6 +35,24 @@ export class OpenFileAction extends Action { } } +export class OpenLocalFileAction extends Action { + + static readonly ID = 'workbench.action.files.openLocalFile'; + static LABEL = nls.localize('openLocalFile', "Open Local File..."); + + constructor( + id: string, + label: string, + @IFileDialogService private readonly dialogService: IFileDialogService + ) { + super(id, label); + } + + run(event?: any, data?: ITelemetryData): Promise { + return this.dialogService.pickFileAndOpen({ forceNewWindow: false, telemetryExtraData: data, availableFileSystems: [Schemas.file] }); + } +} + export class OpenFolderAction extends Action { static readonly ID = 'workbench.action.files.openFolder'; @@ -52,6 +71,25 @@ export class OpenFolderAction extends Action { } } +export class OpenLocalFolderAction extends Action { + + static readonly ID = 'workbench.action.files.openLocalFolder'; + static LABEL = nls.localize('openLocalFolder', "Open Local Folder..."); + + constructor( + id: string, + label: string, + @IFileDialogService private readonly dialogService: IFileDialogService + ) { + super(id, label); + } + + run(event?: any, data?: ITelemetryData): Promise { + return this.dialogService.pickFolderAndOpen({ forceNewWindow: false, telemetryExtraData: data, availableFileSystems: [Schemas.file] }); + } +} + + export class OpenFileFolderAction extends Action { static readonly ID = 'workbench.action.files.openFileFolder'; @@ -70,6 +108,24 @@ export class OpenFileFolderAction extends Action { } } +export class OpenLocalFileFolderAction extends Action { + + static readonly ID = 'workbench.action.files.openLocalFileFolder'; + static LABEL = nls.localize('openLocalFileFolder', "Open Local..."); + + constructor( + id: string, + label: string, + @IFileDialogService private readonly dialogService: IFileDialogService + ) { + super(id, label); + } + + run(event?: any, data?: ITelemetryData): Promise { + return this.dialogService.pickFileFolderAndOpen({ forceNewWindow: false, telemetryExtraData: data, availableFileSystems: [Schemas.file] }); + } +} + export class AddRootFolderAction extends Action { static readonly ID = 'workbench.action.addRootFolder'; diff --git a/src/vs/workbench/browser/actions/workspaceCommands.ts b/src/vs/workbench/browser/actions/workspaceCommands.ts index 88a06912c2..99bfb69c65 100644 --- a/src/vs/workbench/browser/actions/workspaceCommands.ts +++ b/src/vs/workbench/browser/actions/workspaceCommands.ts @@ -70,7 +70,7 @@ CommandsRegistry.registerCommand({ } // Add and show Files Explorer viewlet - return workspaceEditingService.addFolders(folders.map(folder => ({ uri: folder }))) + return workspaceEditingService.addFolders(folders.map(folder => ({ uri: resources.removeTrailingPathSeparator(folder) }))) .then(() => viewletService.openViewlet(viewletService.getDefaultViewletId(), true)) .then(() => undefined); }); diff --git a/src/vs/workbench/browser/dnd.ts b/src/vs/workbench/browser/dnd.ts index 84a07b5d7d..443e14fc4b 100644 --- a/src/vs/workbench/browser/dnd.ts +++ b/src/vs/workbench/browser/dnd.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { hasWorkspaceFileExtension, IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +import { hasWorkspaceFileExtension } from 'vs/platform/workspaces/common/workspaces'; import { normalize } from 'vs/base/common/path'; import { basename } from 'vs/base/common/resources'; import { IFileService } from 'vs/platform/files/common/files'; @@ -29,6 +29,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { addDisposableListener, EventType } from 'vs/base/browser/dom'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IRecentFile } from 'vs/platform/history/common/history'; +import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; export interface IDraggedResource { resource: URI; @@ -154,12 +155,12 @@ export class ResourcesDropHandler { @IFileService private readonly fileService: IFileService, @IWindowsService private readonly windowsService: IWindowsService, @IWindowService private readonly windowService: IWindowService, - @IWorkspacesService private readonly workspacesService: IWorkspacesService, @ITextFileService private readonly textFileService: ITextFileService, @IBackupFileService private readonly backupFileService: IBackupFileService, @IUntitledEditorService private readonly untitledEditorService: IUntitledEditorService, @IEditorService private readonly editorService: IEditorService, - @IConfigurationService private readonly configurationService: IConfigurationService + @IConfigurationService private readonly configurationService: IConfigurationService, + @IWorkspaceEditingService private readonly workspaceEditingService: IWorkspaceEditingService ) { } @@ -284,26 +285,13 @@ export class ResourcesDropHandler { // Pass focus to window this.windowService.focusWindow(); - let workspacesToOpen: Promise | undefined; - // Open in separate windows if we drop workspaces or just one folder if (workspaces.length > 0 || folders.length === 1) { - workspacesToOpen = Promise.resolve([...workspaces, ...folders]); + return this.windowService.openWindow([...workspaces, ...folders], { forceReuseWindow: true }).then(_ => true); } - // Multiple folders: Create new workspace with folders and open - else if (folders.length > 1) { - workspacesToOpen = this.workspacesService.createUntitledWorkspace(folders).then(workspace => [{ uri: workspace.configPath, typeHint: 'file' }]); - } - - // Open - if (workspacesToOpen) { - workspacesToOpen.then(workspaces => { - this.windowService.openWindow(workspaces, { forceReuseWindow: true }); - }); - } - - return true; + // folders.length > 1: Multiple folders: Create new workspace with folders and open + return this.workspaceEditingService.createAndEnterWorkspace(folders).then(_ => true); }); } } diff --git a/src/vs/workbench/browser/nodeless.simpleservices.ts b/src/vs/workbench/browser/nodeless.simpleservices.ts index 68cb6ee725..f9a9e16e9c 100644 --- a/src/vs/workbench/browser/nodeless.simpleservices.ts +++ b/src/vs/workbench/browser/nodeless.simpleservices.ts @@ -1810,7 +1810,7 @@ export class SimpleWorkspacesService implements IWorkspacesService { _serviceBrand: any; - createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[]): Promise { + createUntitledWorkspace(folders?: IWorkspaceFolderCreationData[], remoteAuthority?: string): Promise { // @ts-ignore return Promise.resolve(undefined); } diff --git a/src/vs/workbench/common/contextkeys.ts b/src/vs/workbench/common/contextkeys.ts index 98c48d19f5..a78e86052d 100644 --- a/src/vs/workbench/common/contextkeys.ts +++ b/src/vs/workbench/common/contextkeys.ts @@ -21,3 +21,5 @@ export const IsDevelopmentContext = new RawContextKey('isDevelopment', export const WorkbenchStateContext = new RawContextKey('workbenchState', undefined); export const WorkspaceFolderCountContext = new RawContextKey('workspaceFolderCount', 0); + +export const RemoteFileDialogContext = new RawContextKey('remoteFileDialogVisible', false); diff --git a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts index 0b47ba7b46..7b75200087 100644 --- a/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts +++ b/src/vs/workbench/contrib/callHierarchy/browser/callHierarchy.contribution.ts @@ -119,8 +119,8 @@ registerEditorAction(class extends EditorAction { constructor() { super({ id: 'editor.showCallHierarchy', - label: localize('title', "Call Hierarchy"), - alias: 'Call Hierarchy', + label: localize('title', "Peek Call Hierarchy"), + alias: 'Peek Call Hierarchy', menuOpts: { group: 'navigation', order: 1.48 diff --git a/src/vs/workbench/contrib/comments/browser/commentNode.ts b/src/vs/workbench/contrib/comments/browser/commentNode.ts index 8e52603e45..678850bd2c 100644 --- a/src/vs/workbench/contrib/comments/browser/commentNode.ts +++ b/src/vs/workbench/contrib/comments/browser/commentNode.ts @@ -144,7 +144,7 @@ export class CommentNode extends Disposable { let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle) { + if (commentThread.commentThreadHandle !== undefined) { let toggleReactionAction = this.createReactionPicker2(); actions.push(toggleReactionAction); } else { @@ -327,7 +327,7 @@ export class CommentNode extends Disposable { let action = new ReactionAction(`reaction.${reaction.label}`, `${reaction.label}`, reaction.hasReacted && reaction.canEdit ? 'active' : '', reaction.canEdit, async () => { try { let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle) { + if (commentThread.commentThreadHandle !== undefined) { await this.commentService.toggleReaction(this.owner, this.resource, this.commentThread as modes.CommentThread2, this.comment, reaction); } else { if (reaction.hasReacted) { @@ -360,7 +360,7 @@ export class CommentNode extends Disposable { let reactionGroup = this.commentService.getReactionGroup(this.owner); if (reactionGroup && reactionGroup.length) { let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle) { + if (commentThread.commentThreadHandle !== undefined) { let toggleReactionAction = this.createReactionPicker2(); this._reactionsActionBar.push(toggleReactionAction, { label: false, icon: true }); } else { @@ -386,7 +386,7 @@ export class CommentNode extends Disposable { this._commentEditor.setSelection(new Selection(lastLine, lastColumn, lastLine, lastColumn)); let commentThread = this.commentThread as modes.CommentThread2; - if (commentThread.commentThreadHandle) { + if (commentThread.commentThreadHandle !== undefined) { commentThread.input = { uri: this._commentEditor.getModel()!.uri, value: this.comment.body.value diff --git a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts index 732f1a3013..348798e1fd 100644 --- a/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts +++ b/src/vs/workbench/contrib/comments/browser/commentThreadWidget.ts @@ -689,7 +689,7 @@ export class ReviewZoneWidget extends ZoneWidget implements ICommentThreadWidget async submitComment(): Promise { const activeComment = this.getActiveComment(); if (activeComment instanceof ReviewZoneWidget) { - if ((this._commentThread as modes.CommentThread2).commentThreadHandle) { + if ((this._commentThread as modes.CommentThread2).commentThreadHandle !== undefined) { let commentThread = this._commentThread as modes.CommentThread2; if (commentThread.acceptInputCommand) { diff --git a/src/vs/workbench/contrib/debug/browser/debugStatus.ts b/src/vs/workbench/contrib/debug/browser/debugStatus.ts index ac6489513f..f0b7891e8f 100644 --- a/src/vs/workbench/contrib/debug/browser/debugStatus.ts +++ b/src/vs/workbench/contrib/debug/browser/debugStatus.ts @@ -21,7 +21,7 @@ export class DebugStatus extends Themable implements IStatusbarItem { private statusBarItem: HTMLElement; private label: HTMLElement; private icon: HTMLElement; - private showInStatusBar: string; + private showInStatusBar: 'never' | 'always' | 'onFirstSessionStart'; constructor( @IQuickOpenService private readonly quickOpenService: IQuickOpenService, @@ -36,6 +36,10 @@ export class DebugStatus extends Themable implements IStatusbarItem { this._register(this.debugService.onDidChangeState(state => { if (state !== State.Inactive && this.showInStatusBar === 'onFirstSessionStart') { this.doRender(); + } else { + if (this.showInStatusBar !== 'never') { + this.updateStyles(); + } } })); this.showInStatusBar = configurationService.getValue('debug').showInStatusBar; @@ -53,7 +57,6 @@ export class DebugStatus extends Themable implements IStatusbarItem { } protected updateStyles(): void { - super.updateStyles(); if (this.icon) { if (isStatusbarInDebugMode(this.debugService)) { this.icon.style.backgroundColor = this.getColor(STATUS_BAR_DEBUGGING_FOREGROUND); diff --git a/src/vs/workbench/contrib/debug/node/debugger.ts b/src/vs/workbench/contrib/debug/node/debugger.ts index 1e10b033ff..d4c15f71a3 100644 --- a/src/vs/workbench/contrib/debug/node/debugger.ts +++ b/src/vs/workbench/contrib/debug/node/debugger.ts @@ -11,7 +11,7 @@ import { isObject } from 'vs/base/common/types'; import { TelemetryAppenderClient } from 'vs/platform/telemetry/node/telemetryIpc'; import { IJSONSchema, IJSONSchemaSnippet } from 'vs/base/common/jsonSchema'; import { IWorkspaceFolder } from 'vs/platform/workspace/common/workspace'; -import { IConfig, IDebuggerContribution, IDebugAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession, IAdapterDescriptor, IDebugAdapterServer } from 'vs/workbench/contrib/debug/common/debug'; +import { IConfig, IDebuggerContribution, IDebugAdapterExecutable, INTERNAL_CONSOLE_OPTIONS_SCHEMA, IConfigurationManager, IDebugAdapter, ITerminalSettings, IDebugger, IDebugSession, IAdapterDescriptor, IDebugAdapterServer, IDebugConfiguration } from 'vs/workbench/contrib/debug/common/debug'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { IOutputService } from 'vs/workbench/contrib/output/common/output'; @@ -187,8 +187,11 @@ export class Debugger implements IDebugger { } private inExtHost(): boolean { - /* const debugConfigs = this.configurationService.getValue('debug'); + if (typeof debugConfigs.extensionHostDebugAdapter === 'boolean') { + return debugConfigs.extensionHostDebugAdapter; + } + /* return !!debugConfigs.extensionHostDebugAdapter || this.configurationManager.needsToRunInExtHost(this.type) || (!!this.mainExtensionDescription && this.mainExtensionDescription.extensionLocation.scheme !== 'file'); diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 59a61f5826..33610cf3e3 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -429,6 +429,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { private toDispose: IDisposable[]; private dropEnabled: boolean; + private isCopy: boolean; constructor( @INotificationService private notificationService: INotificationService, @@ -549,7 +550,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } getDragURI(element: ExplorerItem): string | null { - if (this.explorerService.isEditable(element)) { + if (this.explorerService.isEditable(element) || (!this.isCopy && element.isReadonly)) { return null; } @@ -565,6 +566,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void { + this.isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); const items = (data as ElementsDragAndDropData).elements; if (items && items.length && originalEvent.dataTransfer) { // Apply some datatransfer types to allow for dragging the element outside of the application @@ -597,7 +599,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { } // In-Explorer DND (Move/Copy file) else { - this.handleExplorerDrop(data, target, originalEvent); + this.handleExplorerDrop(data, target); } } @@ -711,15 +713,14 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return Promise.resolve(undefined); } - private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem, originalEvent: DragEvent): Promise { + private handleExplorerDrop(data: IDragAndDropData, target: ExplorerItem): Promise { const elementsData = (data as ElementsDragAndDropData).elements; const items = distinctParents(elementsData, s => s.resource); - const isCopy = (originalEvent.ctrlKey && !isMacintosh) || (originalEvent.altKey && isMacintosh); let confirmPromise: Promise; // Handle confirm setting - const confirmDragAndDrop = !isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); + const confirmDragAndDrop = !this.isCopy && this.configurationService.getValue(FileDragAndDrop.CONFIRM_DND_SETTING_KEY); if (confirmDragAndDrop) { confirmPromise = this.dialogService.confirm({ message: items.length > 1 && items.every(s => s.isRoot) ? localize('confirmRootsMove', "Are you sure you want to change the order of multiple root folders in your workspace?") @@ -747,7 +748,7 @@ export class FileDragAndDrop implements ITreeDragAndDrop { return updateConfirmSettingsPromise.then(() => { if (res.confirmed) { const rootDropPromise = this.doHandleRootDrop(items.filter(s => s.isRoot), target); - return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, isCopy)).concat(rootDropPromise)).then(() => undefined); + return Promise.all(items.filter(s => !s.isRoot).map(source => this.doHandleExplorerDrop(source, target, this.isCopy)).concat(rootDropPromise)).then(() => undefined); } return Promise.resolve(undefined); diff --git a/src/vs/workbench/contrib/files/common/explorerService.ts b/src/vs/workbench/contrib/files/common/explorerService.ts index 6f1d459994..21582210ca 100644 --- a/src/vs/workbench/contrib/files/common/explorerService.ts +++ b/src/vs/workbench/contrib/files/common/explorerService.ts @@ -40,6 +40,7 @@ export class ExplorerService implements IExplorerService { private editable: { stat: ExplorerItem, data: IEditableData } | undefined; private _sortOrder: SortOrder; private cutItems: ExplorerItem[] | undefined; + private fileSystemProviderSchemes = new Set(); constructor( @IFileService private fileService: IFileService, @@ -98,7 +99,14 @@ export class ExplorerService implements IExplorerService { this.disposables.push(this.fileService.onAfterOperation(e => this.onFileOperation(e))); this.disposables.push(this.fileService.onFileChanges(e => this.onFileChanges(e))); this.disposables.push(this.configurationService.onDidChangeConfiguration(e => this.onConfigurationUpdated(this.configurationService.getValue()))); - this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(() => this._onDidChangeItem.fire(undefined))); + this.disposables.push(this.fileService.onDidChangeFileSystemProviderRegistrations(e => { + if (e.added && this.fileSystemProviderSchemes.has(e.scheme)) { + // A file system provider got re-registered, we should update all file stats since they might change (got read-only) + this._onDidChangeItem.fire(undefined); + } else { + this.fileSystemProviderSchemes.add(e.scheme); + } + })); this.disposables.push(model.onDidChangeRoots(() => this._onDidChangeRoots.fire())); return model; diff --git a/src/vs/workbench/contrib/markers/browser/markers.ts b/src/vs/workbench/contrib/markers/browser/markers.ts index e738fd6a50..a044359aa9 100644 --- a/src/vs/workbench/contrib/markers/browser/markers.ts +++ b/src/vs/workbench/contrib/markers/browser/markers.ts @@ -61,15 +61,16 @@ export class ActivityUpdater extends Disposable implements IWorkbenchContributio constructor( @IActivityService private readonly activityService: IActivityService, - @IMarkersWorkbenchService private readonly markersWorkbenchService: IMarkersWorkbenchService + @IMarkerService private readonly markerService: IMarkerService ) { super(); - this._register(this.markersWorkbenchService.markersModel.onDidChange(() => this.updateBadge())); + this._register(this.markerService.onMarkerChanged(() => this.updateBadge())); this.updateBadge(); } private updateBadge(): void { - const total = this.markersWorkbenchService.markersModel.resourceMarkers.reduce((r, rm) => r + rm.markers.length, 0); + const { errors, warnings, infos, unknowns } = this.markerService.getStatistics(); + const total = errors + warnings + infos + unknowns; const message = localize('totalProblems', 'Total {0} Problems', total); this.activityService.showActivity(Constants.MARKERS_PANEL_ID, new NumberBadge(total, () => message)); } diff --git a/src/vs/workbench/contrib/markers/browser/media/markers.css b/src/vs/workbench/contrib/markers/browser/media/markers.css index 4c3ac50a11..7ea45538c7 100644 --- a/src/vs/workbench/contrib/markers/browser/media/markers.css +++ b/src/vs/workbench/contrib/markers/browser/media/markers.css @@ -160,7 +160,7 @@ .markers-panel .monaco-tl-contents .marker-icon { height: 22px; - flex: 0 0 16px; + width: 16px; } .markers-panel .marker-icon.warning { diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index 4f9bea6df5..ee12d2b0fc 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -943,7 +943,9 @@ export namespace KeyedTaskIdentifier { } export function create(value: TaskIdentifier): KeyedTaskIdentifier { const resultKey = sortedStringify(value); - return { _key: resultKey, type: value.taskType }; + let result = { _key: resultKey, type: value.taskType }; + Objects.assign(result, value); + return result; } } diff --git a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts index e38dc5a277..a466c7b25b 100644 --- a/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/electron-browser/terminalTaskSystem.ts @@ -890,7 +890,8 @@ export class TerminalTaskSystem implements ITaskSystem { this.currentTask.shellLaunchConfig = { isRendererOnly: true, waitOnExit, - name: this.createTerminalName(task) + name: this.createTerminalName(task), + initialText: task.command.presentation && task.command.presentation.echo ? `\x1b[1m> Executing task: ${task._label} <\x1b[0m\n` : undefined }; } else { let resolvedResult: { command: CommandString, args: CommandString[] } = this.resolveCommandAndArgs(resolver, task.command); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index bbce257372..8bb540269e 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -955,7 +955,7 @@ export class TerminalInstance implements ITerminalInstance { if (typeof this._shellLaunchConfig.waitOnExit === 'string') { let message = this._shellLaunchConfig.waitOnExit; // Bold the message and add an extra new line to make it stand out from the rest of the output - message = `\n\x1b[1m${message}\x1b[0m`; + message = `\r\n\x1b[1m${message}\x1b[0m`; this._xterm.writeln(message); } // Disable all input if the terminal is exiting and listen for next keypress diff --git a/src/vs/workbench/electron-browser/main.contribution.ts b/src/vs/workbench/electron-browser/main.contribution.ts index 99e62dee19..2d644437d4 100644 --- a/src/vs/workbench/electron-browser/main.contribution.ts +++ b/src/vs/workbench/electron-browser/main.contribution.ts @@ -14,14 +14,14 @@ import { isWindows, isLinux, isMacintosh } from 'vs/base/common/platform'; import { KeybindingsReferenceAction, OpenDocumentationUrlAction, OpenIntroductoryVideosUrlAction, OpenTipsAndTricksUrlAction, OpenTwitterUrlAction, OpenRequestFeatureUrlAction, OpenPrivacyStatementUrlAction, OpenLicenseUrlAction } from 'vs/workbench/electron-browser/actions/helpActions'; import { ToggleSharedProcessAction, InspectContextKeysAction, ToggleScreencastModeAction, ToggleDevToolsAction } from 'vs/workbench/electron-browser/actions/developerActions'; import { ShowAboutDialogAction, ZoomResetAction, ZoomOutAction, ZoomInAction, ToggleFullScreenAction, CloseCurrentWindowAction, SwitchWindow, NewWindowAction, QuickSwitchWindow, QuickOpenRecentAction, inRecentFilesPickerContextKey, OpenRecentAction, ReloadWindowWithExtensionsDisabledAction, NewWindowTabHandler, ReloadWindowAction, ShowPreviousWindowTabHandler, ShowNextWindowTabHandler, MoveWindowTabToNewWindowHandler, MergeWindowTabsHandlerHandler, ToggleWindowTabsBarHandler } from 'vs/workbench/electron-browser/actions/windowActions'; -import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, DuplicateWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction, CloseWorkspaceAction } from 'vs/workbench/browser/actions/workspaceActions'; +import { AddRootFolderAction, GlobalRemoveRootFolderAction, OpenWorkspaceAction, SaveWorkspaceAsAction, OpenWorkspaceConfigFileAction, DuplicateWorkspaceInNewWindowAction, OpenFileFolderAction, OpenFileAction, OpenFolderAction, CloseWorkspaceAction, OpenLocalFileAction, OpenLocalFolderAction, OpenLocalFileFolderAction } from 'vs/workbench/browser/actions/workspaceActions'; import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { inQuickOpenContext, getQuickNavigateHandler } from 'vs/workbench/browser/parts/quickopen/quickopen'; import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { ADD_ROOT_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/workspaceCommands'; -import { SupportsWorkspacesContext, IsMacContext, HasMacNativeTabsContext, IsDevelopmentContext, WorkbenchStateContext, WorkspaceFolderCountContext } from 'vs/workbench/common/contextkeys'; +import { SupportsWorkspacesContext, IsMacContext, HasMacNativeTabsContext, IsDevelopmentContext, WorkbenchStateContext, WorkspaceFolderCountContext, RemoteFileDialogContext } from 'vs/workbench/common/contextkeys'; import { NoEditorsVisibleContext, SingleEditorGroupsContext } from 'vs/workbench/common/editor'; import { IWindowService, IWindowsService } from 'vs/platform/windows/common/windows'; import { LogStorageAction } from 'vs/platform/storage/node/storageService'; @@ -40,9 +40,12 @@ import { InstallVSIXAction } from 'vs/workbench/contrib/extensions/electron-brow if (isMacintosh) { registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileFolderAction, OpenFileFolderAction.ID, OpenFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open...', fileCategory); + registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileFolderAction, OpenLocalFileFolderAction.ID, OpenLocalFileFolderAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local...', fileCategory, RemoteFileDialogContext); } else { registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFileAction, OpenFileAction.ID, OpenFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }), 'File: Open File...', fileCategory); registry.registerWorkbenchAction(new SyncActionDescriptor(OpenFolderAction, OpenFolderAction.ID, OpenFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }), 'File: Open Folder...', fileCategory); + registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFileAction, OpenLocalFileAction.ID, OpenLocalFileAction.LABEL, { primary: KeyMod.CtrlCmd | KeyCode.KEY_O }, RemoteFileDialogContext), 'File: Open Local File...', fileCategory, RemoteFileDialogContext); + registry.registerWorkbenchAction(new SyncActionDescriptor(OpenLocalFolderAction, OpenLocalFolderAction.ID, OpenLocalFolderAction.LABEL, { primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_O) }, RemoteFileDialogContext), 'File: Open Local Folder...', fileCategory, RemoteFileDialogContext); } registry.registerWorkbenchAction(new SyncActionDescriptor(QuickOpenRecentAction, QuickOpenRecentAction.ID, QuickOpenRecentAction.LABEL), 'File: Quick Open Recent...', fileCategory); diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts index 0e513ce5a5..29d04dd611 100644 --- a/src/vs/workbench/electron-browser/main.ts +++ b/src/vs/workbench/electron-browser/main.ts @@ -181,7 +181,7 @@ class CodeRendererMain extends Disposable { const fileService = new FileService2(logService); serviceCollection.set(IFileService, fileService); - fileService.registerProvider(Schemas.file, new DiskFileSystemProvider()); + fileService.registerProvider(Schemas.file, new DiskFileSystemProvider(logService)); // Remote const remoteAuthorityResolverService = new RemoteAuthorityResolverService(); diff --git a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts index dd960abcd2..fdef1d97d1 100644 --- a/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts +++ b/src/vs/workbench/services/dialogs/browser/remoteFileDialog.ts @@ -22,6 +22,8 @@ import { getIconClasses } from 'vs/editor/common/services/getIconClasses'; import { Schemas } from 'vs/base/common/network'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService'; +import { IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { RemoteFileDialogContext } from 'vs/workbench/common/contextkeys'; interface FileQuickPickItem extends IQuickPickItem { uri: URI; @@ -48,6 +50,7 @@ export class RemoteFileDialog { private scheme: string = REMOTE_HOST_SCHEME; private shouldOverwriteFile: boolean = false; private autoComplete: string; + private contextKey: IContextKey; constructor( @IFileService private readonly fileService: IFileService, @@ -61,9 +64,11 @@ export class RemoteFileDialog { @IModeService private readonly modeService: IModeService, @IEnvironmentService private readonly environmentService: IEnvironmentService, @IRemoteAgentService private readonly remoteAgentService: IRemoteAgentService, + @IContextKeyService contextKeyService: IContextKeyService ) { this.remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + this.contextKey = RemoteFileDialogContext.bindTo(contextKeyService); } public async showOpenDialog(options: IOpenDialogOptions = {}): Promise { @@ -245,10 +250,12 @@ export class RemoteFileDialog { if (!isResolving) { resolve(undefined); } + this.contextKey.set(false); this.filePickBox.dispose(); }); this.filePickBox.show(); + this.contextKey.set(true); this.updateItems(homedir, trailing); if (trailing) { this.filePickBox.valueSelection = [this.filePickBox.value.length - trailing.length, this.filePickBox.value.length - ext.length]; diff --git a/src/vs/workbench/services/files2/common/fileService2.ts b/src/vs/workbench/services/files2/common/fileService2.ts index a5a753cd1e..6ce7ce534b 100644 --- a/src/vs/workbench/services/files2/common/fileService2.ts +++ b/src/vs/workbench/services/files2/common/fileService2.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Disposable, IDisposable, toDisposable, combinedDisposable } from 'vs/base/common/lifecycle'; -import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability } from 'vs/platform/files/common/files'; +import { IFileService, IResolveFileOptions, IResourceEncodings, FileChangesEvent, FileOperationEvent, IFileSystemProviderRegistrationEvent, IFileSystemProvider, IFileStat, IResolveFileResult, IResolveContentOptions, IContent, IStreamContent, ITextSnapshot, IUpdateContentOptions, ICreateFileOptions, IFileSystemProviderActivationEvent, FileOperationError, FileOperationResult, FileOperation, FileSystemProviderCapabilities, FileType, toFileSystemProviderErrorCode, FileSystemProviderErrorCode, IStat, IFileStatWithMetadata, IResolveMetadataFileOptions, etag, hasReadWriteCapability, hasFileFolderCopyCapability, hasOpenReadWriteCloseCapability, toFileOperationResult, IFileSystemProviderWithOpenReadWriteCloseCapability, IFileSystemProviderWithFileReadWriteCapability, IResolveFileResultWithMetadata } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation'; @@ -252,7 +252,7 @@ export class FileService2 extends Disposable implements IFileService { } async resolveFiles(toResolve: { resource: URI, options?: IResolveFileOptions }[]): Promise; - async resolveFiles(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; + async resolveFiles(toResolve: { resource: URI, options: IResolveMetadataFileOptions }[]): Promise; async resolveFiles(toResolve: { resource: URI; options?: IResolveFileOptions; }[]): Promise { return Promise.all(toResolve.map(async entry => { try { @@ -600,14 +600,17 @@ export class FileService2 extends Disposable implements IFileService { private async doWriteBuffered(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, resource: URI, buffer: Uint8Array): Promise { - // Open handle + // open handle const handle = await provider.open(resource, { create: true }); // write into handle until all bytes from buffer have been written - await this.doWriteBuffer(provider, handle, buffer, buffer.byteLength, 0, 0); - - // Close handle - return provider.close(handle); + try { + await this.doWriteBuffer(provider, handle, buffer, buffer.byteLength, 0, 0); + } catch (error) { + throw error; + } finally { + await provider.close(handle); + } } private async doWriteBuffer(provider: IFileSystemProviderWithOpenReadWriteCloseCapability, handle: number, buffer: Uint8Array, length: number, posInFile: number, posInBuffer: number): Promise { @@ -623,39 +626,45 @@ export class FileService2 extends Disposable implements IFileService { } private async doPipeBuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, target: URI): Promise { + let sourceHandle: number | undefined = undefined; + let targetHandle: number | undefined = undefined; - // Open handles - const sourceHandle = await sourceProvider.open(source, { create: false }); - const targetHandle = await targetProvider.open(target, { create: true }); + try { - const buffer = new Uint8Array(8 * 1024); + // Open handles + sourceHandle = await sourceProvider.open(source, { create: false }); + targetHandle = await targetProvider.open(target, { create: true }); - let posInFile = 0; - let posInBuffer = 0; - let bytesRead = 0; - do { - // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at - // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). - bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer, posInBuffer, buffer.byteLength - posInBuffer); + const buffer = new Uint8Array(16 * 1024); - // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at - // buffer position (posInBuffer) all bytes we read (bytesRead). - await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer); + let posInFile = 0; + let posInBuffer = 0; + let bytesRead = 0; + do { + // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at + // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). + bytesRead = await sourceProvider.read(sourceHandle, posInFile, buffer, posInBuffer, buffer.byteLength - posInBuffer); - posInFile += bytesRead; - posInBuffer += bytesRead; + // write into target (targetHandle) at current position (posInFile) from buffer (buffer) at + // buffer position (posInBuffer) all bytes we read (bytesRead). + await this.doWriteBuffer(targetProvider, targetHandle, buffer, bytesRead, posInFile, posInBuffer); - // when buffer full, fill it again from the beginning - if (posInBuffer === buffer.length) { - posInBuffer = 0; - } - } while (bytesRead > 0); + posInFile += bytesRead; + posInBuffer += bytesRead; - // Close handles - return Promise.all([ - sourceProvider.close(sourceHandle), - targetProvider.close(targetHandle) - ]).then(() => undefined); + // when buffer full, fill it again from the beginning + if (posInBuffer === buffer.length) { + posInBuffer = 0; + } + } while (bytesRead > 0); + } catch (error) { + throw error; + } finally { + await Promise.all([ + typeof sourceHandle === 'number' ? sourceProvider.close(sourceHandle) : Promise.resolve(), + typeof targetHandle === 'number' ? targetProvider.close(targetHandle) : Promise.resolve(), + ]); + } } private async doPipeUnbuffered(sourceProvider: IFileSystemProviderWithFileReadWriteCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { @@ -668,11 +677,14 @@ export class FileService2 extends Disposable implements IFileService { const targetHandle = await targetProvider.open(target, { create: true }); // Read entire buffer from source and write buffered - const buffer = await sourceProvider.readFile(source); - await this.doWriteBuffer(targetProvider, targetHandle, buffer, buffer.byteLength, 0, 0); - - // Close handle - return targetProvider.close(targetHandle); + try { + const buffer = await sourceProvider.readFile(source); + await this.doWriteBuffer(targetProvider, targetHandle, buffer, buffer.byteLength, 0, 0); + } catch (error) { + throw error; + } finally { + await targetProvider.close(targetHandle); + } } private async doPipeBufferedToUnbuffered(sourceProvider: IFileSystemProviderWithOpenReadWriteCloseCapability, source: URI, targetProvider: IFileSystemProviderWithFileReadWriteCapability, target: URI, overwrite: boolean): Promise { @@ -683,23 +695,26 @@ export class FileService2 extends Disposable implements IFileService { // Open handle const sourceHandle = await sourceProvider.open(source, { create: false }); - const buffer = new Uint8Array(size); + try { + const buffer = new Uint8Array(size); - let pos = 0; - let bytesRead = 0; - do { - // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at - // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). - bytesRead = await sourceProvider.read(sourceHandle, pos, buffer, pos, buffer.byteLength - pos); + let pos = 0; + let bytesRead = 0; + do { + // read from source (sourceHandle) at current position (posInFile) into buffer (buffer) at + // buffer position (posInBuffer) up to the size of the buffer (buffer.byteLength). + bytesRead = await sourceProvider.read(sourceHandle, pos, buffer, pos, buffer.byteLength - pos); - pos += bytesRead; - } while (bytesRead > 0); + pos += bytesRead; + } while (bytesRead > 0 && pos < size); - // Write buffer into target at once - await this.doWriteUnbuffered(targetProvider, target, buffer, overwrite); - - // Close handle - return sourceProvider.close(sourceHandle); + // Write buffer into target at once + await this.doWriteUnbuffered(targetProvider, target, buffer, overwrite); + } catch (error) { + throw error; + } finally { + await sourceProvider.close(sourceHandle); + } } private throwIfFileSystemIsReadonly(provider: IFileSystemProvider): IFileSystemProvider { diff --git a/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts index cb53f87043..baf23be07b 100644 --- a/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/electron-browser/diskFileSystemProvider.ts @@ -9,9 +9,14 @@ import { FileDeleteOptions, FileSystemProviderCapabilities } from 'vs/platform/f import { isWindows } from 'vs/base/common/platform'; import { localize } from 'vs/nls'; import { basename } from 'vs/base/common/path'; +import { ILogService } from 'vs/platform/log/common/log'; export class DiskFileSystemProvider extends NodeDiskFileSystemProvider { + constructor(logService: ILogService) { + super(logService); + } + get capabilities(): FileSystemProviderCapabilities { if (!this._capabilities) { this._capabilities = super.capabilities | FileSystemProviderCapabilities.Trash; diff --git a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts index 4d5ed22466..888cf34a0c 100644 --- a/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts +++ b/src/vs/workbench/services/files2/node/diskFileSystemProvider.ts @@ -16,9 +16,14 @@ import { normalize } from 'vs/base/common/path'; import { joinPath } from 'vs/base/common/resources'; import { isEqual } from 'vs/base/common/extpath'; import { retry } from 'vs/base/common/async'; +import { ILogService } from 'vs/platform/log/common/log'; export class DiskFileSystemProvider extends Disposable implements IFileSystemProvider { + constructor(private logService: ILogService) { + super(); + } + //#region File Capabilities onDidChangeCapabilities: Event = Event.None; @@ -73,8 +78,12 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro for (let i = 0; i < children.length; i++) { const child = children[i]; - const stat = await this.stat(joinPath(resource, child)); - result.push([child, stat.type]); + try { + const stat = await this.stat(joinPath(resource, child)); + result.push([child, stat.type]); + } catch (error) { + this.logService.trace(error); // ignore errors for individual entries that can arise from permission denied + } } return result; @@ -112,7 +121,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro if (exists && isWindows) { try { // On Windows and if the file exists, we use a different strategy of saving the file - // by first truncating the file and then writing with r+ mode. This helps to save hidden files on Windows + // by first truncating the file and then writing with r+ flag. This helps to save hidden files on Windows // (see https://github.com/Microsoft/vscode/issues/931) and prevent removing alternate data streams // (see https://github.com/Microsoft/vscode/issues/6363) await truncate(filePath, 0); @@ -123,6 +132,8 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro // short timeout, assuming that the file is free to write then. await retry(() => writeFile(filePath, content, { flag: 'r+' }), 100 /* ms delay */, 3 /* retries */); } catch (error) { + this.logService.trace(error); + // we heard from users that fs.truncate() fails (https://github.com/Microsoft/vscode/issues/59561) // in that case we simply save the file without truncating first (same as macOS and Linux) await writeFile(filePath, content); @@ -142,20 +153,20 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro try { const filePath = this.toFilePath(resource); - let mode: string; + let flags: string; if (opts.create) { // we take this as a hint that the file is opened for writing // as such we use 'w' to truncate an existing or create the // file otherwise. we do not allow reading. - mode = 'w'; + flags = 'w'; } else { // otherwise we assume the file is opened for reading // as such we use 'r' to neither truncate, nor create // the file. - mode = 'r'; + flags = 'r'; } - return await promisify(open)(filePath, mode); + return await promisify(open)(filePath, flags); } catch (error) { throw this.toFileSystemProviderError(error); } @@ -300,7 +311,7 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro return error; // avoid double conversion } - let code: FileSystemProviderErrorCode | undefined = undefined; + let code: FileSystemProviderErrorCode; switch (error.code) { case 'ENOENT': code = FileSystemProviderErrorCode.FileNotFound; @@ -315,6 +326,8 @@ export class DiskFileSystemProvider extends Disposable implements IFileSystemPro case 'EACCESS': code = FileSystemProviderErrorCode.NoPermissions; break; + default: + code = FileSystemProviderErrorCode.Unknown; } return createFileSystemProviderError(error, code); diff --git a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts index 053a16b6f6..ab56c897a0 100644 --- a/src/vs/workbench/services/files2/test/node/diskFileService.test.ts +++ b/src/vs/workbench/services/files2/test/node/diskFileService.test.ts @@ -17,8 +17,10 @@ import { URI } from 'vs/base/common/uri'; import { existsSync, statSync, readdirSync, readFileSync } from 'fs'; import { FileOperation, FileOperationEvent, IFileStat, FileOperationResult, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; import { NullLogService } from 'vs/platform/log/common/log'; -import { isLinux } from 'vs/base/common/platform'; +import { isLinux, isWindows } from 'vs/base/common/platform'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { promisify } from 'util'; +import { exec } from 'child_process'; function getByName(root: IFileStat, name: string): IFileStat | null { if (root.children === undefined) { @@ -70,13 +72,15 @@ suite('Disk File Service', () => { let disposables: IDisposable[] = []; setup(async () => { - service = new FileService2(new NullLogService()); + const logService = new NullLogService(); + + service = new FileService2(logService); disposables.push(service); - fileProvider = new TestDiskFileSystemProvider(); + fileProvider = new TestDiskFileSystemProvider(logService); service.registerProvider(Schemas.file, fileProvider); - testProvider = new TestDiskFileSystemProvider(); + testProvider = new TestDiskFileSystemProvider(logService); service.registerProvider(testSchema, testProvider); const id = generateUuid(); @@ -307,6 +311,45 @@ suite('Disk File Service', () => { assert.equal(r2.name, 'deep'); }); + test('resolveFile - folder symbolic link', async () => { + if (isWindows) { + return; // only for unix systems + } + + const link = URI.file(join(testDir, 'deep-link')); + await promisify(exec)(`ln -s deep ${basename(link.fsPath)}`, { cwd: testDir }); + + const resolved = await service.resolveFile(link); + assert.equal(resolved.children!.length, 4); + assert.equal(resolved.isDirectory, true); + assert.equal(resolved.isSymbolicLink, true); + }); + + test('resolveFile - file symbolic link', async () => { + if (isWindows) { + return; // only for unix systems + } + + const link = URI.file(join(testDir, 'lorem.txt-linked')); + await promisify(exec)(`ln -s lorem.txt ${basename(link.fsPath)}`, { cwd: testDir }); + + const resolved = await service.resolveFile(link); + assert.equal(resolved.isDirectory, false); + assert.equal(resolved.isSymbolicLink, true); + }); + + test('resolveFile - invalid symbolic link does not break', async () => { + if (isWindows) { + return; // only for unix systems + } + + await promisify(exec)('ln -s foo bar', { cwd: testDir }); + + const resolved = await service.resolveFile(URI.file(testDir)); + assert.equal(resolved.isDirectory, true); + assert.equal(resolved.children!.length, 8); + }); + test('deleteFile', async () => { let event: FileOperationEvent; disposables.push(service.onAfterOperation(e => event = e)); diff --git a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts index 9aa943e4bb..95f2765276 100644 --- a/src/vs/workbench/services/workspace/node/workspaceEditingService.ts +++ b/src/vs/workbench/services/workspace/node/workspaceEditingService.ts @@ -21,7 +21,7 @@ import { BackupFileService } from 'vs/workbench/services/backup/node/backupFileS import { ICommandService } from 'vs/platform/commands/common/commands'; import { distinct } from 'vs/base/common/arrays'; import { isLinux, isWindows, isMacintosh } from 'vs/base/common/platform'; -import { isEqual, basename, isEqualOrParent } from 'vs/base/common/resources'; +import { isEqual, basename, isEqualOrParent, getComparisonKey } from 'vs/base/common/resources'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import { IFileService } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -205,7 +205,7 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (state !== WorkbenchState.WORKSPACE) { let newWorkspaceFolders = this.contextService.getWorkspace().folders.map(folder => ({ uri: folder.uri } as IWorkspaceFolderCreationData)); newWorkspaceFolders.splice(typeof index === 'number' ? index : newWorkspaceFolders.length, 0, ...foldersToAdd); - newWorkspaceFolders = distinct(newWorkspaceFolders, folder => isLinux ? folder.uri.toString() : folder.uri.toString().toLowerCase()); + newWorkspaceFolders = distinct(newWorkspaceFolders, folder => getComparisonKey(folder.uri)); if (state === WorkbenchState.EMPTY && newWorkspaceFolders.length === 0 || state === WorkbenchState.FOLDER && newWorkspaceFolders.length === 1) { return Promise.resolve(); // return if the operation is a no-op for the current state @@ -245,8 +245,8 @@ export class WorkspaceEditingService implements IWorkspaceEditingService { if (path && !this.isValidTargetWorkspacePath(path)) { return Promise.reject(null); } - - const untitledWorkspace = await this.workspaceService.createUntitledWorkspace(folders); + const remoteAuthority = this.windowService.getConfiguration().remoteAuthority; + const untitledWorkspace = await this.workspaceService.createUntitledWorkspace(folders, remoteAuthority); if (path) { await this.saveWorkspaceAs(untitledWorkspace, path); } else {