diff --git a/README.md b/README.md index 0c43500c..42c5cded 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ authorization: token xxx ``` +## Request History +![request-history](images/request-history.png) +Each time we sent a http request, the request details(method, url, headers and body) would be persisted into file. By using shortcut `Ctrl+Alt+H`, or press `F1` and then select/type `Rest Client: Request History`, you can view the last __50__ request items in the time reversing order, you can select any request you wish to trigger again. After specified request history item is selected, the request details would be displayed in a temp file, you can view the request details or follow previous step to trigger the request again. + ## Settings * `rest-client.clearoutput`: Clear previous output for each request. (Default is __false__) * `rest-client.followredirect`: Follow HTTP 3xx responses as redirects. (Default is __true__) @@ -73,6 +77,9 @@ authorization: token xxx [MIT License](LICENSE) ## Change Log +### 0.2.0 +* Add http request history + ### 0.1.1 * Update image in README.md diff --git a/images/request-history.png b/images/request-history.png new file mode 100644 index 00000000..6e68c536 Binary files /dev/null and b/images/request-history.png differ diff --git a/package.json b/package.json index 46d9d1d9..7f46726e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "rest-client", "displayName": "REST Client", "description": "REST Client for Visual Studio Code", - "version": "0.1.1", + "version": "0.2.0", "publisher": "humao", "author": { "name": "Huachao Mao", @@ -34,7 +34,8 @@ "Http" ], "activationEvents": [ - "onCommand:rest-client.request" + "onCommand:rest-client.request", + "onCommand:rest-client.history" ], "main": "./out/src/extension", "contributes": { @@ -42,12 +43,20 @@ { "command": "rest-client.request", "title": "Rest Client: Send Request" + }, + { + "command": "rest-client.history", + "title": "Rest Client: Request History" } ], "keybindings": [ { "command": "rest-client.request", "key": "ctrl+alt+r" + }, + { + "command": "rest-client.history", + "key": "ctrl+alt+h" } ], "configuration": { @@ -82,6 +91,7 @@ "vscode": "^0.11.0" }, "dependencies": { - "request": "^2.69.0" + "request": "^2.69.0", + "tmp": "^0.0.28" } } \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 00000000..5b0bfbce --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,5 @@ +'use strict'; + +export const ExtensionId: string = 'humao.rest-client'; +export const HistoryFileName: string = 'history.json'; +export const HistoryItemsMaxCount: number = 50; \ No newline at end of file diff --git a/src/controllers/historyController.ts b/src/controllers/historyController.ts new file mode 100644 index 00000000..217e07e3 --- /dev/null +++ b/src/controllers/historyController.ts @@ -0,0 +1,83 @@ +"use strict"; + +import { window, workspace, QuickPickItem, OutputChannel } from 'vscode'; +import { PersistUtility } from '../persistUtility' +import { HttpRequest } from '../models/httpRequest' +import { HistoryQuickPickItem } from '../models/historyQuickPickItem' +import { EOL } from 'os'; +import * as fs from 'fs' + +var tmp = require('tmp') + +export class HistoryController { + private _outputChannel: OutputChannel; + + constructor() { + this._outputChannel = window.createOutputChannel('REST'); + } + + run(): any { + PersistUtility.load().then(requests => { + if (!requests || requests.length <= 0) { + window.showInformationMessage("No request history items are found!"); + return; + } + var itemPickList: HistoryQuickPickItem[] = requests.map(request => { + // TODO: add headers and body in pick item? + let item = new HistoryQuickPickItem(); + item.label = `${request.method.toUpperCase()} ${request.url}`; + item.rawRequest = request; + return item; + }); + + window.showQuickPick(itemPickList, { placeHolder: "" }).then(item => { + if (!item) { + return; + } + this.createRequestInTempFile(item.rawRequest).then(path => { + workspace.openTextDocument(path).then(d => { + window.showTextDocument(d); + }); + }); + }) + }).catch(error => this.errorHandler(error)); + } + + private createRequestInTempFile(request: HttpRequest): Promise { + return new Promise((resolve, reject) => { + tmp.file({ prefix: 'vscode-restclient-' }, function _tempFileCreated(err, tmpFilePath, fd) { + if (err) { + reject(err); + return; + } + let output = `${request.method.toUpperCase()} ${request.url}${EOL}`; + if (request.headers) { + for (var header in request.headers) { + if (request.headers.hasOwnProperty(header)) { + var value = request.headers[header]; + output += `${header}: ${value}${EOL}`; + } + } + } + if (request.body) { + output += `${EOL}${request.body}`; + } + fs.writeFile(tmpFilePath, output, error => { + reject(error); + return; + }); + resolve(tmpFilePath); + }); + }); + } + + private errorHandler(error: any) { + this._outputChannel.appendLine(error); + this._outputChannel.show(); + window.showErrorMessage("There was an error, please view details in output log"); + } + + dispose() { + this._outputChannel.dispose(); + } +} \ No newline at end of file diff --git a/src/controllers/requestController.ts b/src/controllers/requestController.ts new file mode 100644 index 00000000..80220dfa --- /dev/null +++ b/src/controllers/requestController.ts @@ -0,0 +1,97 @@ +"use strict"; + +import { window, workspace, Uri, StatusBarItem, StatusBarAlignment, OutputChannel } from 'vscode'; +import { RequestParser } from '../parser' +import { MimeUtility } from '../mimeUtility' +import { HttpClient } from '../httpClient' +import { HttpRequest } from '../models/httpRequest' +import { RestClientSettings } from '../models/configurationSettings' +import { PersistUtility } from '../persistUtility' + +export class RequestController { + private _outputChannel: OutputChannel; + private _statusBarItem: StatusBarItem; + private _restClientSettings: RestClientSettings; + private _httpClient: HttpClient; + + constructor() { + this._outputChannel = window.createOutputChannel('REST'); + this._statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left); + this._restClientSettings = new RestClientSettings(); + this._httpClient = new HttpClient(this._restClientSettings); + } + + run() { + let editor = window.activeTextEditor; + if (!editor || !editor.document) { + return; + } + + // Get selected text of selected lines or full document + let selectedText: string; + if (editor.selection.isEmpty) { + selectedText = editor.document.getText(); + } else { + selectedText = editor.document.getText(editor.selection); + } + + if (selectedText === '') { + return; + } + + if (this._restClientSettings.clearOutput) { + this._outputChannel.clear(); + } + + // clear status bar + this._statusBarItem.text = `$(cloud-upload)`; + this._statusBarItem.show(); + + // parse http request + let httpRequest = RequestParser.parseHttpRequest(selectedText); + if (!httpRequest) { + return; + } + + // set http request + this._httpClient.send(httpRequest) + .then(response => { + let output = `HTTP/${response.httpVersion} ${response.statusCode} ${response.statusMessage}\n` + for (var header in response.headers) { + if (response.headers.hasOwnProperty(header)) { + var value = response.headers[header]; + output += `${header}: ${value}\n` + } + } + + let body = response.body; + let contentType = response.headers['content-type']; + if (contentType) { + let type = MimeUtility.parse(contentType).type; + if (type === 'application/json') { + body = JSON.stringify(JSON.parse(body), null, 4); + } + } + + output += `\n${body}`; + this._outputChannel.appendLine(`${output}\n`); + this._outputChannel.show(true); + + this._statusBarItem.text = ` $(clock) ${response.elapsedMillionSeconds}ms`; + this._statusBarItem.tooltip = 'duration'; + + // persist to history json file + PersistUtility.save(httpRequest); + }) + .catch(error => { + this._statusBarItem.text = ''; + this._outputChannel.appendLine(`${error}\n`); + this._outputChannel.show(true); + }); + } + + dispose() { + this._outputChannel.dispose(); + this._statusBarItem.dispose(); + } +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index b4246537..b2cfac9d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,12 +1,9 @@ 'use strict'; // The module 'vscode' contains the VS Code extensibility API // Import the module and reference it with the alias vscode in your code below -import { ExtensionContext, commands, window, workspace, Uri, StatusBarAlignment } from 'vscode'; -import { RequestParser } from './parser' -import { MimeUtility } from './mimeUtility' -import { HttpClient } from './httpClient' -import { HttpRequest } from './models/httpRequest' -import { RestClientSettings } from './models/configurationSettings' +import { ExtensionContext, commands, Disposable } from 'vscode'; +import { RequestController } from './controllers/requestController' +import { HistoryController } from './controllers/historyController' // this method is called when your extension is activated // your extension is activated the very first time the command is executed @@ -16,81 +13,12 @@ export function activate(context: ExtensionContext) { // This line of code will only be executed once when your extension is activated console.log('Congratulations, your extension "rest-client" is now active!'); - let outChannel = window.createOutputChannel('REST'); - let statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left) - let restClientSettings = new RestClientSettings(); - - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = commands.registerCommand('rest-client.request', () => { - let editor = window.activeTextEditor; - if (!editor || !editor.document) { - return; - } - - // Get selected text of selected lines or full document - let selectedText: string; - if (editor.selection.isEmpty) { - selectedText = editor.document.getText(); - } else { - selectedText = editor.document.getText(editor.selection); - } - - if (selectedText === '') { - return; - } - - if (restClientSettings.clearOutput) { - outChannel.clear(); - } - - // clear status bar - statusBarItem.text = `$(cloud-upload)`; - statusBarItem.show(); - - // parse http request - let httpRequest = RequestParser.parseHttpRequest(selectedText); - if (!httpRequest) { - return; - } - - // set http request - let httpClient = new HttpClient(restClientSettings); - httpClient.send(httpRequest) - .then(response => { - let output = `HTTP/${response.httpVersion} ${response.statusCode} ${response.statusMessage}\n` - for (var header in response.headers) { - if (response.headers.hasOwnProperty(header)) { - var value = response.headers[header]; - output += `${header}: ${value}\n` - } - } - - let body = response.body; - let contentType = response.headers['content-type']; - if (contentType) { - let type = MimeUtility.parse(contentType).type; - if (type === 'application/json') { - body = JSON.stringify(JSON.parse(body), null, 4); - } - } - - output += `\n${body}`; - outChannel.appendLine(`${output}\n`); - outChannel.show(true); - - statusBarItem.text = ` $(clock) ${response.elapsedMillionSeconds}ms`; - statusBarItem.tooltip = 'duration'; - }) - .catch(error => { - statusBarItem.text = ''; - outChannel.appendLine(`${error}\n`); - outChannel.show(true); - }); - }); - - context.subscriptions.push(disposable); + let requestController = new RequestController(); + let historyController = new HistoryController(); + context.subscriptions.push(requestController); + context.subscriptions.push(historyController); + context.subscriptions.push(commands.registerCommand('rest-client.request', () => requestController.run())); + context.subscriptions.push(commands.registerCommand('rest-client.history', () => historyController.run())); } // this method is called when your extension is deactivated diff --git a/src/httpClient.ts b/src/httpClient.ts index 34a805d9..80a9d423 100644 --- a/src/httpClient.ts +++ b/src/httpClient.ts @@ -25,7 +25,7 @@ export class HttpClient { }; if (!options.headers) { - options.headers = {}; + options.headers = httpRequest.headers = {}; } // add default user agent if not specified diff --git a/src/models/historyQuickPickItem.ts b/src/models/historyQuickPickItem.ts new file mode 100644 index 00000000..7caed36c --- /dev/null +++ b/src/models/historyQuickPickItem.ts @@ -0,0 +1,11 @@ +"use strict"; + +import { QuickPickItem } from 'vscode'; +import { HttpRequest } from '../models/httpRequest' + +export class HistoryQuickPickItem implements QuickPickItem { + label: string; + description: string; + detail: string; + rawRequest: HttpRequest; +} \ No newline at end of file diff --git a/src/persistUtility.ts b/src/persistUtility.ts new file mode 100644 index 00000000..19711458 --- /dev/null +++ b/src/persistUtility.ts @@ -0,0 +1,55 @@ +'use strict'; + +import { extensions, Extension, window, OutputChannel } from 'vscode'; +import { HttpRequest } from './models/httpRequest' +import * as Constants from './constants' +import * as fs from 'fs' +import * as path from 'path' + +export class PersistUtility { + private static historyFilePath: string = path.join(extensions.getExtension(Constants.ExtensionId).extensionPath, Constants.HistoryFileName); + private static emptyHttpRequestItems: HttpRequest[] = []; + + static save(httpRequest: HttpRequest) { + PersistUtility.deserializeFromHistoryFile().then(requests => { + requests.unshift(httpRequest); + requests = requests.slice(0, Constants.HistoryItemsMaxCount); + PersistUtility.serializeToHistoryFile(requests); + }).catch(error => {}); + } + + static load(): Promise { + return PersistUtility.deserializeFromHistoryFile(); + } + + private static createHistoryFileIfNotExist() { + try { + fs.statSync(PersistUtility.historyFilePath); + } catch (error) { + fs.writeFileSync(PersistUtility.historyFilePath, ''); + } + } + + private static serializeToHistoryFile(requests: HttpRequest[]): Promise { + return new Promise((resolve, reject) => { + fs.writeFile(PersistUtility.historyFilePath, JSON.stringify(requests)); + }); + } + + private static deserializeFromHistoryFile(): Promise { + return new Promise((resolve, reject) => { + fs.readFile(PersistUtility.historyFilePath, (error, data) => { + if (error) { + PersistUtility.createHistoryFileIfNotExist(); + } else { + let fileContent = data.toString(); + if (fileContent) { + resolve(JSON.parse(fileContent)); + return; + } + } + resolve(PersistUtility.emptyHttpRequestItems); + }) + }); + } +} \ No newline at end of file