Skip to content

Commit

Permalink
Nturinski/baseEditor (#43)
Browse files Browse the repository at this point in the history
* Leverage baseEditor from Storage Explorer
  • Loading branch information
nturinski authored Dec 13, 2017
1 parent bc92fbf commit 3477f48
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 3 deletions.
41 changes: 40 additions & 1 deletion ui/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { Subscription } from 'azure-arm-resource/lib/subscription/models';
import { ServiceClientCredentials } from 'ms-rest';
import { AzureEnvironment } from 'ms-rest-azure';
import { Uri, TreeDataProvider, Disposable, TreeItem, Event } from 'vscode';
import { Uri, TreeDataProvider, Disposable, TreeItem, Event, OutputChannel, Memento, TextDocument } from 'vscode';

export declare class AzureTreeDataProvider implements TreeDataProvider<IAzureNode>, Disposable {
public static readonly subscriptionContextValue: string;
Expand Down Expand Up @@ -107,3 +107,42 @@ export interface IAzureParentTreeItem extends IAzureTreeItem, IChildProvider {
}

export declare class UserCancelledError extends Error { }

export declare abstract class BaseEditor<ContextT> implements Disposable {
/**
* Implement this interface if you need to download and upload remote files
* @param showSavePromptKey Key used globally by VS Code to determine whether or not to show the savePrompt
* @param outputChannel OutputChannel where output will be displayed when editor performs actions
*/
constructor(showSavePromptKey: string, outputChannel?: OutputChannel | undefined);

/**
* Implement this to retrieve data from your remote server, returns the file as a string
*/
abstract getData(context: ContextT): Promise<string>;

/**
* Implement this to allow for remote updating
*/
abstract updateData(context: ContextT, data: string): Promise<string>;

/**
* Implement this to return the file name from the remote
*/
abstract getFilename(context: ContextT): Promise<string>;

/**
* Implement this to return the size in MB.
*/
abstract getSize(context: ContextT): Promise<number>;

/**
* Implement this to edit what is displayed to the user when uploading the file to the remote
*/
abstract getSaveConfirmationText(context: ContextT): Promise<string>;

onDidSaveTextDocument(globalState: Memento, doc: TextDocument): Promise<void>;
showEditor(context: ContextT, sizeLimit?: number): Promise<void>;
dispose(): Promise<void>;
}

4 changes: 3 additions & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "vscode-azureextensionui",
"author": "Microsoft Corporation",
"version": "0.3.1",
"version": "0.4.0",
"description": "Common UI tools for developing Azure extensions for VS Code",
"tags": [
"azure",
Expand Down Expand Up @@ -31,12 +31,14 @@
},
"dependencies": {
"azure-arm-resource": "^3.0.0-preview",
"fs-extra": "^4.0.3",
"ms-rest": "^2.2.2",
"ms-rest-azure": "^2.4.4",
"opn": "^5.1.0",
"vscode-nls": "^2.0.2"
},
"devDependencies": {
"@types/fs-extra": "^4.0.6",
"typescript": "^2.5.3",
"tslint": "^5.7.0",
"tslint-microsoft-contrib": "5.0.1",
Expand Down
116 changes: 116 additions & 0 deletions ui/src/BaseEditor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as fse from 'fs-extra';
import * as path from 'path';
import * as vscode from 'vscode';
import { DialogResponses } from './DialogResponses' ;
import { UserCancelledError } from './errors';
import { localize } from "./localize";
import { createTemporaryFile } from './utils/createTemporaryFile';

// tslint:disable-next-line:no-unsafe-any
export abstract class BaseEditor<ContextT> implements vscode.Disposable {
private fileMap: { [key: string]: [vscode.TextDocument, ContextT] } = {};
private ignoreSave: boolean = false;

constructor(private readonly showSavePromptKey: string, private readonly outputChannel?: vscode.OutputChannel) {
}

public abstract getData(context: ContextT): Promise<string>;
public abstract updateData(context: ContextT, data: string): Promise<string>;
public abstract getFilename(context: ContextT): Promise<string>;
public abstract getSaveConfirmationText(context: ContextT): Promise<string>;
public abstract getSize(context: ContextT): Promise<number>;

public async showEditor(context: ContextT, sizeLimit?: number /* in Megabytes */): Promise<void> {
const fileName: string = await this.getFilename(context);

this.appendLineToOutput(localize('opening', 'Opening "{0}"...', fileName));
if (sizeLimit !== undefined) {
const size: number = await this.getSize(context);
if (size > sizeLimit) {
const message: string = localize('tooLargeError', '"{0}" is too large to download.', fileName);
throw new Error(message);
}
}

const localFilePath: string = await createTemporaryFile(fileName);
const document: vscode.TextDocument = await vscode.workspace.openTextDocument(localFilePath);
this.fileMap[localFilePath] = [document, context];
const data: string = await this.getData(context);
const textEditor: vscode.TextEditor = await vscode.window.showTextDocument(document);
await this.updateEditor(data, textEditor);
}

public async updateMatchingContext(doc: vscode.Uri): Promise<void> {
const filePath: string | undefined = Object.keys(this.fileMap).find((fsPath: string) => path.relative(doc.fsPath, fsPath) === '');
if (filePath) {
const [textDocument, context]: [vscode.TextDocument, ContextT] = this.fileMap[filePath];
await this.updateRemote(context, textDocument);
}
}

public async dispose(): Promise<void> {
Object.keys(this.fileMap).forEach(async (key: string) => await fse.remove(path.dirname(key)));
}

public async onDidSaveTextDocument(globalState: vscode.Memento, doc: vscode.TextDocument): Promise<void> {
const filePath: string | undefined = Object.keys(this.fileMap).find((fsPath: string) => path.relative(doc.uri.fsPath, fsPath) === '');
if (!this.ignoreSave && filePath) {
const context: ContextT = this.fileMap[filePath][1];
const showSaveWarning: boolean | undefined = vscode.workspace.getConfiguration().get(this.showSavePromptKey);

if (showSaveWarning) {
const message: string = await this.getSaveConfirmationText(context);
const result: vscode.MessageItem | undefined = await vscode.window.showWarningMessage(message, DialogResponses.upload, DialogResponses.dontWarn, DialogResponses.dontUpload);
if (result === DialogResponses.dontWarn) {
await vscode.workspace.getConfiguration().update(this.showSavePromptKey, false, vscode.ConfigurationTarget.Global);
await globalState.update(this.showSavePromptKey, true);
} else if (result === DialogResponses.dontUpload) {
throw new UserCancelledError();
}
}
await this.updateRemote(context, doc);
}
}

protected appendLineToOutput(value: string): void {
if (!!this.outputChannel) {
this.outputChannel.appendLine(value);
this.outputChannel.show(true);
}
}

private async updateRemote(context: ContextT, doc: vscode.TextDocument): Promise<void> {
const filename: string = await this.getFilename(context);
this.appendLineToOutput(localize('updating', 'Updating "{0}" ...', filename));
const updatedData: string = await this.updateData(context, doc.getText());
this.appendLineToOutput(localize('done', 'Updated "{0}".', filename));
await this.updateEditor(updatedData, vscode.window.activeTextEditor);
}

private async updateEditor(data: string, textEditor?: vscode.TextEditor): Promise<void> {
if (!!textEditor) {
await BaseEditor.writeToEditor(textEditor, data);
this.ignoreSave = true;
try {
await textEditor.document.save();
} finally {
this.ignoreSave = false;
}
}
}
// tslint:disable-next-line:member-ordering
private static async writeToEditor(editor: vscode.TextEditor, data: string): Promise<void> {
await editor.edit((editBuilder: vscode.TextEditorEdit) => {
if (editor.document.lineCount > 0) {
const lastLine: vscode.TextLine = editor.document.lineAt(editor.document.lineCount - 1);
editBuilder.delete(new vscode.Range(new vscode.Position(0, 0), new vscode.Position(lastLine.range.start.line, lastLine.range.end.character)));
}
editBuilder.insert(new vscode.Position(0, 0), data);
});
}
}
14 changes: 14 additions & 0 deletions ui/src/DialogResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { MessageItem } from 'vscode';
import { localize } from './localize';

export namespace DialogResponses {
export const upload: MessageItem = { title: localize('upload', "Upload") };
export const dontWarn: MessageItem = { title: localize('dontwarn', "Upload, don't warn again") };
export const dontUpload: MessageItem = { title: localize('dontUpload', "Don't Upload"), isCloseAffordance: true };
export const OK: MessageItem = { title: localize('OK', 'OK') };
}
1 change: 1 addition & 0 deletions ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@

export { AzureTreeDataProvider } from './treeDataProvider/AzureTreeDataProvider';
export { UserCancelledError } from './errors';
export { BaseEditor } from './BaseEditor';
18 changes: 18 additions & 0 deletions ui/src/utils/createTemporaryFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as crypto from "crypto";
import * as fse from 'fs-extra';
import * as os from 'os';
import * as path from 'path';

export async function createTemporaryFile(fileName: string): Promise<string> {
const randomFolderNameLength: number = 12;
const buffer: Buffer = crypto.randomBytes(Math.ceil(randomFolderNameLength / 2));
const folderName: string = buffer.toString('hex').slice(0, randomFolderNameLength);
const filePath: string = path.join(os.tmpdir(), folderName, fileName);
await fse.ensureFile(filePath);
return filePath;
}
23 changes: 23 additions & 0 deletions ui/thirdpartynotices.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ open source projects along with the license information below. We acknowledge an
are grateful to these developers for their contribution to open source.

1. opn version 5.1.0 (https://github.com/sindresorhus/opn
2. fs-extra (https://github.com/jprichardson/node-fs-extra)

opn NOTICES BEGIN HERE
=============================
Expand Down Expand Up @@ -34,3 +35,25 @@ THE SOFTWARE.

END OF opn NOTICES AND INFORMATION
=========================================

fs-extra NOTICES BEGIN HERE
=============================

(The MIT License)

Copyright (c) 2011-2017 JP Richardson

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
(the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

END OF fs-extra NOTICES AND INFORMATION
==================================
2 changes: 1 addition & 1 deletion ui/tslint.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
"no-inferrable-types": false,
"no-multiline-string": true,
"no-null-keyword": false,
"no-parameter-properties": true,
"no-parameter-properties": false, // changed
"no-relative-imports": false, // changed
"no-require-imports": true,
"no-shadowed-variable": true,
Expand Down

0 comments on commit 3477f48

Please sign in to comment.