├── .gitignore ├── .vscodeignore ├── tsconfig.json ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── test ├── extension.test.ts └── index.ts ├── README.md ├── package.json └── src ├── formatters.ts └── extension.ts /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | /typings/modules/* 5 | /typings/index.d.ts 6 | /*.vsix -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | typings/** 4 | out/test/** 5 | test/** 6 | src/** 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | vsc-extension-quickstart.md 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "sourceMap": true, 7 | "rootDir": "." 8 | }, 9 | "exclude": [ 10 | "node_modules", 11 | ".vscode-test" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "eslint.enable": false, 4 | "editor.formatOnSave": true, 5 | "files.insertFinalNewline": true, 6 | "files.exclude": { 7 | "out": false // set this to true to hide the "out" folder with the compiled JS files 8 | }, 9 | "search.exclude": { 10 | "out": true // set this to false to include "out" folder in search results 11 | }, 12 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 13 | } -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as myExtension from '../src/extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", () => { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", () => { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outDir": "${workspaceRoot}/out/src", 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outDir": "${workspaceRoot}/out/test", 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-beautify 2 | 3 | This extension wraps prettydiff/esformatter to format your javascript, JSX, typescript, TSX file. 4 | 5 | ### Local Version 6 | 7 | If this extension found a locally installed prettydiff/esformatter, this extension uses that instead of bundled module. 8 | 9 | It is strongly recommended that you install formatter implementation locally. 10 | 11 | ## How To Use 12 | 13 | * open Context Menu and choose `Format Code` on `javascript`/`javascriptreact`/`typescript`/`typescriptreact` 14 | * shortcuts: Alt+Shift+F 15 | * Press F1, enter `react.beautify` 16 | 17 | ## Extension Settings 18 | 19 | This extension contributes the following settings: 20 | 21 | * `react.beautify.onSave`: by default is `false`. if you set `true`, Automatically format files on save. 22 | * `react.beautify.formatter`: select the formatter implementation. Accepted values are `prettydiff` and `esformatter`. default value is `prettydiff`. 23 | * `react.beautify.configFilePath`: Specifies the workspace relative config filepath. default value is `.jsbeautifyrc`. 24 | * Comments in your settings file are acceptable (they're removed before the file is parsed). 25 | 26 | ## Formatter Settings 27 | 28 | * [Pretty Diff](http://prettydiff.com/documentation.xhtml) 29 | * [esformatter](https://github.com/millermedeiros/esformatter/blob/master/doc/config.md) 30 | * [esformatter-jsx](https://github.com/royriojas/esformatter-jsx#config) 31 | 32 | ## Releases 33 | ### 0.3.0: 2016-10-18 34 | * Adopt formatOnSave changes 35 | * thanks for @jrieken !! 36 | 37 | ### 0.2.0: 2016-09-30 38 | * add typescript support 39 | 40 | ### 0.1.0: 2016-09-23 41 | * add javascript support 42 | 43 | ### 0.0.1: 2016-09-22 44 | * initial release 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "react-beautify", 4 | "displayName": "react-beautify", 5 | "description": "Beautify JS/JSX/TS/TSX Code", 6 | "version": "0.3.0", 7 | "publisher": "taichi", 8 | "galleryBanner": { 9 | "color": "#61dafb", 10 | "theme": "light" 11 | }, 12 | "engines": { 13 | "vscode": "^1.6.0" 14 | }, 15 | "categories": [ 16 | "Other", 17 | "Formatters", 18 | "Languages" 19 | ], 20 | "license": "MIT", 21 | "activationEvents": [ 22 | "onLanguage:javascript", 23 | "onLanguage:javascriptreact", 24 | "onLanguage:typescript", 25 | "onLanguage:typescriptreact", 26 | "onCommand:react.beautify" 27 | ], 28 | "main": "./out/src/extension", 29 | "contributes": { 30 | "configuration": { 31 | "title": "react-beautify configuration", 32 | "properties": { 33 | "react.beautify.onSave": { 34 | "description": "Automatically format files on save.", 35 | "type": "boolean", 36 | "default": false 37 | }, 38 | "react.beautify.formatter": { 39 | "description": "Formatter implementation", 40 | "type": "string", 41 | "enum": [ 42 | "prettydiff", 43 | "esformatter" 44 | ], 45 | "default": "prettydiff" 46 | }, 47 | "react.beautify.configFilePath": { 48 | "description": "Specifies the workspace relative config filepath", 49 | "type": "string", 50 | "default": ".jsbeautifyrc" 51 | } 52 | } 53 | }, 54 | "commands": [ 55 | { 56 | "command": "react.beautify", 57 | "title": "Beautify JS/JSX/TS/TSX" 58 | } 59 | ] 60 | }, 61 | "scripts": { 62 | "vscode:prepublish": "tsc -p ./", 63 | "compile": "tsc -watch -p ./", 64 | "postinstall": "node ./node_modules/vscode/bin/install" 65 | }, 66 | "dependencies": { 67 | "esformatter": "^0.9.6", 68 | "esformatter-jsx": "^7.0.1", 69 | "lodash": "^4.16.0", 70 | "prettydiff": "^1.16.37", 71 | "resolve-from": "^2.0.0", 72 | "strip-json-comments": "^2.0.1" 73 | }, 74 | "devDependencies": { 75 | "typescript": "^2.0.3", 76 | "vscode": "^1.0.0", 77 | "@types/lodash": "^4.0.0", 78 | "@types/strip-json-comments": "^0.0.28", 79 | "@types/resolve-from": "0.0.18", 80 | "@types/node": "^6.0.40", 81 | "@types/mocha": "^2.2.32" 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "https://github.com/taichi/react-beautify" 86 | }, 87 | "bugs": { 88 | "url": "https://github.com/taichi/react-beautify/issues" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/formatters.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import resolve = require('resolve-from'); 4 | import fs = require('fs'); 5 | import _ = require('lodash'); 6 | 7 | const formatters = { 8 | prettydiff: prettydiffFactory, 9 | esformatter: esformatterFactory 10 | } 11 | 12 | export interface Formatter { (src: string, options: any): string; } 13 | 14 | export function make(root: string, impl: string, langId: string): Formatter { 15 | let f = formatters[impl]; 16 | if (f) { 17 | return f(root, impl, langId); 18 | } 19 | return (src, opt) => src; // NullFormatter 20 | } 21 | 22 | function loadModue(root: string, impl: string, onFail: () => any): any { 23 | // load workspace module 24 | if (root) { 25 | try { 26 | let p = resolve(root, impl); 27 | if (p) { 28 | return require(p); 29 | } 30 | } catch (e) { 31 | // suppress error 32 | } 33 | } 34 | // load bundled module 35 | return onFail(); 36 | } 37 | 38 | function prettydiffFactory(root: string, impl: string, langId: string): Formatter { 39 | const mod = loadModue(root, impl, () => require(impl)); 40 | return (src, options) => { 41 | let output = mod.api(_.defaultsDeep({}, options, { 42 | insize: options.tabSize, 43 | inchar: options.insertSpaces ? " " : "\t", 44 | source: src, 45 | mode: 'beautify' 46 | }, languageOptions[langId])); 47 | return output[0]; 48 | }; 49 | } 50 | 51 | const languageOptions = { 52 | javascript: { 53 | lang: "javascript" 54 | }, 55 | javascriptreact: { 56 | lang: "jsx", 57 | jsx: true 58 | }, 59 | typescript: { 60 | lang: "typescript", 61 | typescript: true 62 | }, 63 | typescriptreact: { 64 | lang: "jsx", 65 | typescript: true, 66 | jsx: true 67 | } 68 | } 69 | 70 | function esformatterFactory(root: string, impl: string, langId: string): Formatter { 71 | if(langId.startsWith("typescript")) { 72 | throw "esformatter don't support typescript. use prettydiff."; 73 | } 74 | const mod = loadModue(root, impl, () => { 75 | let m = require(impl); 76 | m.register(require("esformatter-jsx")); 77 | return m; 78 | }); 79 | return (src, options) => { 80 | return mod.format(src, _.defaultsDeep({}, options, { 81 | indent: { 82 | value: options.insertSpaces ? _.repeat(" ", options.tabSize) : "\t" 83 | } 84 | })); 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { 3 | commands, workspace, window, 4 | WorkspaceConfiguration, ExtensionContext, TextDocumentWillSaveEvent, 5 | languages, Range, TextDocument, Position, TextEdit, TextLine, WorkspaceEdit 6 | } from 'vscode'; 7 | 8 | import path = require('path'); 9 | import fs = require('fs'); 10 | 11 | 12 | import _ = require('lodash'); 13 | import shaver = require('strip-json-comments'); 14 | 15 | import formatters = require('./formatters'); 16 | 17 | const supported_languages = ["javascript", "javascriptreact", "typescript", "typescriptreact"]; 18 | 19 | export function activate(context: ExtensionContext) { 20 | context.subscriptions.push(commands.registerCommand("react.beautify", () => { 21 | const a = window.activeTextEditor; 22 | if (a && a.document) { 23 | const r = allOf(a.document); 24 | return format(a.document, r, a.options) 25 | .then(txt => a.edit(editor => editor.replace(r, txt))) 26 | .catch(report); 27 | } 28 | })); 29 | _.each(supported_languages, l => registerFormatter(context, l)); 30 | workspace.onWillSaveTextDocument(onWillSave); 31 | } 32 | 33 | function registerFormatter(context: ExtensionContext, languageId: string) { 34 | context.subscriptions.push(languages.registerDocumentFormattingEditProvider(languageId, { 35 | provideDocumentFormattingEdits: (document, options, token) => { 36 | const r = allOf(document); 37 | return format(document, r, options) 38 | .then(txt => [TextEdit.replace(r, txt)]) 39 | .catch(report); 40 | } 41 | })); 42 | context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(languageId, { 43 | provideDocumentRangeFormattingEdits: (document, range, options, token) => { 44 | let begin = new Position(range.start.line, 0); 45 | let end = range.end.translate(0, Number.MAX_VALUE); 46 | let r = document.validateRange(new Range(begin, end)); 47 | return format(document, r, options) 48 | .then(txt => [TextEdit.replace(r, txt)]) 49 | .catch(report); 50 | 51 | } 52 | })); 53 | } 54 | 55 | function onWillSave(event: TextDocumentWillSaveEvent) { 56 | const doc = event.document; 57 | if (supports(doc.languageId) && getConfig("onSave", false)) { 58 | const r = allOf(doc); 59 | let editor = window.visibleTextEditors.find(ed => ed.document && ed.document.fileName === doc.fileName); 60 | let options = editor ? editor.options : workspace.getConfiguration('editor'); 61 | event.waitUntil(format(doc, r, options) 62 | .then(txt => { 63 | let we = new WorkspaceEdit(); 64 | we.replace(doc.uri, r, txt); 65 | return workspace.applyEdit(we); 66 | }) 67 | .catch(report)); 68 | } 69 | } 70 | 71 | export function deactivate() { 72 | } 73 | 74 | export function format(doc: TextDocument, range: Range, defaults: any): Promise { 75 | if (doc) { 76 | const langId = doc.languageId; 77 | if (langId && supports(langId)) { 78 | const root = workspace.rootPath; 79 | return loadOptions(root, defaults) 80 | .then(options => { 81 | let t = getConfig("formatter") || "prettydiff"; 82 | return [options, formatters.make(root, t, langId)]; 83 | }).then(optFmt => { 84 | let src = doc.getText(doc.validateRange(range)); 85 | return optFmt[1](src, optFmt[0]); 86 | }); 87 | } 88 | return Promise.reject(`Unsupported languageId ${doc.languageId}`); 89 | } 90 | return Promise.reject("Fail to get File Information. maybe too large."); 91 | } 92 | 93 | function loadOptions(root: string, defaults: any): Promise { 94 | if (root) { 95 | let relpath = getConfig("configFilePath") || ".jsbeautifyrc"; 96 | const conf = path.join(root, relpath); 97 | if (path.normalize(conf).startsWith(root) && fs.existsSync(conf)) { 98 | return new Promise((next, reject) => 99 | fs.readFile(conf, "utf8", (err, buffer) => { 100 | if (buffer) { 101 | try { 102 | let srcjson = shaver(buffer.toString()); 103 | next(_.defaultsDeep({}, JSON.parse(srcjson), defaults)); 104 | } catch (e) { 105 | reject(`incorrect config file. ${conf} can't parse correctly.`); 106 | } 107 | } else { 108 | next(defaults); 109 | } 110 | }) 111 | ); 112 | } 113 | } 114 | return Promise.resolve(defaults); 115 | } 116 | 117 | function getConfig(section: string, defaults?: T) { 118 | const config = workspace.getConfiguration("react.beautify"); 119 | return config.get(section, defaults); 120 | } 121 | 122 | function allOf(document: TextDocument): Range { 123 | return document.validateRange(new Range(0, 0, Number.MAX_VALUE, Number.MAX_VALUE)); 124 | } 125 | 126 | function supports(languageId: string): boolean { 127 | return -1 < supported_languages.indexOf(languageId); 128 | } 129 | 130 | function report(e: string) { 131 | if (e) { 132 | window.showErrorMessage(e); 133 | console.error("beautify ERROR:", e); 134 | } 135 | return []; 136 | } 137 | --------------------------------------------------------------------------------