diff --git a/src/sql/workbench/common/workspaceActions.ts b/src/sql/workbench/common/workspaceActions.ts new file mode 100644 index 0000000000..0f997b44d9 --- /dev/null +++ b/src/sql/workbench/common/workspaceActions.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Action } from 'vs/base/common/actions'; +import { IOpenerService } from 'vs/platform/opener/common/opener'; +import { URI } from 'vs/base/common/uri'; +// eslint-disable-next-line code-layering,code-import-patterns +import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron'; + +export class ShowFileInFolderAction extends Action { + + constructor(private path: string, label: string, @IElectronService private electronService: IElectronService) { + super('showItemInFolder.action.id', label); + } + + run(): Promise { + return this.electronService.showItemInFolder(this.path); + } +} + +export class OpenFileInFolderAction extends Action { + + constructor(private path: string, label: string, @IOpenerService private openerService: IOpenerService) { + super('openItemInFolder.action.id', label); + } + + run() { + return this.openerService.open(URI.file(this.path), { openExternal: true }); + } +} diff --git a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts index 13233953d8..552502763e 100644 --- a/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts +++ b/src/sql/workbench/contrib/notebook/browser/outputs/gridOutput.component.ts @@ -40,7 +40,6 @@ import { Orientation } from 'vs/base/browser/ui/splitview/splitview'; import { ToggleableAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; import { IInsightOptions } from 'sql/workbench/common/editor/query/chartState'; import { NotebookChangeType } from 'sql/workbench/services/notebook/common/contracts'; -import { URI } from 'vs/base/common/uri'; @Component({ selector: GridOutputComponent.SELECTOR, @@ -309,7 +308,7 @@ class DataResourceDataProvider implements IGridDataProvider { return serializer.handleSerialization(this.documentUri, format, (filePath) => this.doSerialize(serializer, filePath, format, selection)); } - private doSerialize(serializer: ResultSerializer, filePath: URI, format: SaveFormat, selection: Slick.Range[]): Promise { + private doSerialize(serializer: ResultSerializer, filePath: string, format: SaveFormat, selection: Slick.Range[]): Promise { if (!this.canSerialize) { return Promise.resolve(undefined); } @@ -347,7 +346,7 @@ class DataResourceDataProvider implements IGridDataProvider { let serializeRequestParams: SerializeDataParams = assign(serializer.getBasicSaveParameters(format), >{ saveFormat: format, columns: columns, - filePath: filePath.fsPath, + filePath: filePath, getRowRange: (rowStart, numberOfRows) => getRows(rowStart, numberOfRows), rowCount: rowLength }); diff --git a/src/sql/workbench/services/query/common/resultSerializer.ts b/src/sql/workbench/services/query/common/resultSerializer.ts index 363f8158e4..448f31c33e 100644 --- a/src/sql/workbench/services/query/common/resultSerializer.ts +++ b/src/sql/workbench/services/query/common/resultSerializer.ts @@ -13,12 +13,15 @@ import * as nls from 'vs/nls'; import Severity from 'vs/base/common/severity'; import { INotificationService, INotification } from 'vs/platform/notification/common/notification'; +import { getBaseLabel } from 'vs/base/common/labels'; +import { ShowFileInFolderAction, OpenFileInFolderAction } from 'sql/workbench/common/workspaceActions'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { getRootPath, resolveCurrentDirectory } from 'sql/platform/common/pathUtilities'; +import { getRootPath, resolveCurrentDirectory, resolveFilePath } from 'sql/platform/common/pathUtilities'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IFileDialogService, FileFilter } from 'vs/platform/dialogs/common/dialogs'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -let prevSavePath: URI; +let prevSavePath: string; export interface ISaveRequest { format: SaveFormat; @@ -53,6 +56,7 @@ export enum SaveFormat { } const msgSaveFailed = nls.localize('msgSaveFailed', "Failed to save results. "); +const msgSaveSucceeded = nls.localize('msgSaveSucceeded', "Successfully saved results to "); /** * Handles save results request from the context menu of slickGrid @@ -66,7 +70,8 @@ export class ResultSerializer { @IEditorService private _editorService: IEditorService, @IWorkspaceContextService private _contextService: IWorkspaceContextService, @IFileDialogService private readonly fileDialogService: IFileDialogService, - @INotificationService private _notificationService: INotificationService + @INotificationService private _notificationService: INotificationService, + @IInstantiationService private readonly _instantiationService: IInstantiationService ) { } /** @@ -76,6 +81,9 @@ export class ResultSerializer { const self = this; return this.promptForFilepath(saveRequest.format, uri).then(filePath => { if (filePath) { + if (!path.isAbsolute(filePath)) { + filePath = resolveFilePath(uri, filePath, this.rootPath)!; + } let saveResultsParams = this.getParameters(uri, filePath, saveRequest.batchIndex, saveRequest.resultSetNumber, saveRequest.format, saveRequest.selection ? saveRequest.selection[0] : undefined); let sendRequest = () => this.sendSaveRequestToService(saveResultsParams); return self.doSave(filePath, saveRequest.format, sendRequest); @@ -95,11 +103,14 @@ export class ResultSerializer { /** * Handle save request by getting filename from user and sending request to service */ - public handleSerialization(uri: string, format: SaveFormat, sendRequest: ((filePath: URI) => Promise)): Thenable { + public handleSerialization(uri: string, format: SaveFormat, sendRequest: ((filePath: string) => Promise)): Thenable { const self = this; return this.promptForFilepath(format, uri).then(filePath => { if (filePath) { - return self.doSave(filePath, format, () => sendRequest(filePath)); + if (!path.isAbsolute(filePath)) { + filePath = resolveFilePath(uri, filePath, this.rootPath)!; + } + return self.doSave(filePath, format, () => sendRequest(filePath!)); } return Promise.resolve(); }); @@ -109,8 +120,8 @@ export class ResultSerializer { return getRootPath(this._contextService); } - private promptForFilepath(format: SaveFormat, resourceUri: string): Promise { - let filepathPlaceHolder = prevSavePath ? path.dirname(prevSavePath.fsPath) : resolveCurrentDirectory(resourceUri, this.rootPath); + private promptForFilepath(format: SaveFormat, resourceUri: string): Promise { + let filepathPlaceHolder = prevSavePath ? path.dirname(prevSavePath) : resolveCurrentDirectory(resourceUri, this.rootPath); if (filepathPlaceHolder) { filepathPlaceHolder = path.join(filepathPlaceHolder, this.getResultsDefaultFilename(format)); } @@ -121,8 +132,8 @@ export class ResultSerializer { filters: this.getResultsFileExtension(format) }).then(filePath => { if (filePath) { - prevSavePath = filePath; - return filePath; + prevSavePath = filePath.fsPath; + return filePath.fsPath; } return undefined; }); @@ -260,9 +271,9 @@ export class ResultSerializer { } - private getParameters(uri: string, filePath: URI, batchIndex: number, resultSetNo: number, format: string, selection?: Slick.Range): SaveResultsRequestParams { + private getParameters(uri: string, filePath: string, batchIndex: number, resultSetNo: number, format: string, selection?: Slick.Range): SaveResultsRequestParams { let saveResultsParams = this.getBasicSaveParameters(format); - saveResultsParams.filePath = filePath.fsPath; + saveResultsParams.filePath = filePath; saveResultsParams.ownerUri = uri; saveResultsParams.resultSetIndex = resultSetNo; saveResultsParams.batchIndex = batchIndex; @@ -282,10 +293,35 @@ export class ResultSerializer { return !!(selection && !((selection.fromCell === selection.toCell) && (selection.fromRow === selection.toRow))); } + + private promptFileSavedNotification(savedFilePath: string) { + let label = getBaseLabel(path.dirname(savedFilePath)); + + this._notificationService.prompt( + Severity.Info, + msgSaveSucceeded + savedFilePath, + [{ + label: nls.localize('openLocation', "Open file location"), + run: () => { + let action = this._instantiationService.createInstance(ShowFileInFolderAction, savedFilePath, label || path.sep); + action.run(); + action.dispose(); + } + }, { + label: nls.localize('openFile', "Open file"), + run: () => { + let action = this._instantiationService.createInstance(OpenFileInFolderAction, savedFilePath, label || path.sep); + action.run(); + action.dispose(); + } + }] + ); + } + /** * Send request to sql tools service to save a result set */ - private async doSave(filePath: URI, format: string, sendRequest: () => Promise): Promise { + private async doSave(filePath: string, format: string, sendRequest: () => Promise): Promise { const saveNotification: INotification = { severity: Severity.Info, @@ -305,6 +341,7 @@ export class ResultSerializer { message: msgSaveFailed + (result ? result.messages : '') }); } else { + this.promptFileSavedNotification(filePath); this.openSavedFile(filePath, format); } // TODO telemetry for save results @@ -323,9 +360,10 @@ export class ResultSerializer { /** * Open the saved file in a new vscode editor pane */ - private openSavedFile(filePath: URI, format: string): void { + private openSavedFile(filePath: string, format: string): void { if (format !== SaveFormat.EXCEL) { - this._editorService.openEditor({ resource: filePath }).then((result) => { + let uri = URI.file(filePath); + this._editorService.openEditor({ resource: uri }).then((result) => { }, (error: any) => { this._notificationService.notify({