├── logo.png ├── CHANGELOG.md ├── .gitignore ├── .vscodeignore ├── index.js ├── .vscode ├── settings.json └── launch.json ├── jsconfig.json ├── download-phar.js ├── test ├── extension.test.js └── index.js ├── .travis.yml ├── webpack.config.js ├── beautifyHtml.js ├── package.json ├── README.md └── extension.js /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/angebagui/vscode-php-cs-fixer/master/logo.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 0.1.34 3 | disable cache, add option `--using-cache=no`, is useless for single file. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslint* 3 | php-cs-fixer.phar 4 | package-lock.json 5 | *.vsix 6 | extension_pack.js 7 | extension_pack.js.map 8 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | download-phar.js 9 | .travis.yml 10 | webpack.config.js 11 | node_modules/** -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | if (fs.existsSync(__dirname + '/extension_pack.js')) 3 | module.exports = require(__dirname + '/extension_pack'); 4 | else 5 | module.exports = require(__dirname + '/extension'); // for development -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 4 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ] 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /download-phar.js: -------------------------------------------------------------------------------- 1 | const { DownloaderHelper } = require('node-downloader-helper'); 2 | let dl = new DownloaderHelper('https://cs.symfony.com/download/php-cs-fixer-v2.phar', __dirname, { 'fileName': 'php-cs-fixer.phar', 'override': true }); 3 | dl.on('start', () => console.log('start to download php-cs-fixer.phar')); 4 | dl.on('end', () => console.log('download php-cs-fixer.phar successfully.')); 5 | // dl.on('progress', stat => { 6 | // console.log('\rdownloading php-cs-fixer.phar: ' + Math.floor(stat.progress) + '%'); 7 | // }); 8 | dl.start(); -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension 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=${workspaceFolder}" ], 11 | "stopOnEntry": false 12 | }, 13 | { 14 | "name": "Launch Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/test" ], 19 | "stopOnEntry": false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | /* global suite, test */ 2 | 3 | // 4 | // Note: This example test is leveraging the Mocha test framework. 5 | // Please refer to their documentation on https://mochajs.org/ for help. 6 | // 7 | 8 | // The module 'assert' provides assertion methods from node 9 | var assert = require('assert'); 10 | 11 | // You can import and use all API from the 'vscode' module 12 | // as well as import your extension to test it 13 | var vscode = require('vscode'); 14 | var myExtension = require('../extension'); 15 | 16 | // Defines a Mocha test suite to group tests of similar kind together 17 | suite("Extension Tests", function() { 18 | 19 | // Defines a Mocha unit test 20 | test("Something 1", function() { 21 | assert.equal(-1, [1, 2, 3].indexOf(5)); 22 | assert.equal(-1, [1, 2, 3].indexOf(0)); 23 | }); 24 | }); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 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.js (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | # nodejs版本 3 | node_js: 4 | - '10' 5 | 6 | # Travis-CI Caching 7 | cache: 8 | directories: 9 | - node_modules 10 | 11 | # S: Build Lifecycle 12 | install: 13 | - npm install vscode --save-dev 14 | - npm install vsce --save-dev 15 | - npm install webpack@^4.42.1 webpack-cli@^3.3.11 --save-dev 16 | - npm install ts-loader --save-dev 17 | - npm install 18 | 19 | before_script: 20 | 21 | # 无其他依赖项所以执行npm run build 构建就行了 22 | script: 23 | - webpack --mode production 24 | # 删除无用文件 25 | - cp -vf extension_pack.js index.js 26 | - rm -vf extension.js extension_pack.* beautifyHtml.js 27 | - vsce publish -p ${VSC_TOKEN} 28 | 29 | after_script: 30 | # - git config user.name "${U_NAME}" 31 | # - git config user.email "${U_EMAIL}" 32 | # - git add -A 33 | # - git commit -m "Update from ci" 34 | # - git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:${P_BRANCH} 35 | # E: Build LifeCycle 36 | 37 | #指定分支,只有指定的分支提交时才会运行脚本 38 | branches: 39 | only: 40 | - master 41 | env: 42 | global: 43 | # 我将其添加到了travis-ci的环境变量中 44 | #- GH_REF: github.com/yimogit/metools.git -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | node: false, // Prevent NodeStuffPlugin from overriding `__dirname` 📖 -> https://webpack.js.org/configuration/node/ 11 | entry: './extension.js', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 12 | output: { 13 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 14 | path: path.resolve(__dirname), 15 | filename: 'extension_pack.js', 16 | libraryTarget: 'commonjs2', 17 | devtoolModuleFilenameTemplate: '../[resource-path]' 18 | }, 19 | // devtool: 'source-map', // source map file 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; 42 | -------------------------------------------------------------------------------- /beautifyHtml.js: -------------------------------------------------------------------------------- 1 | const beautifyHtml = require('js-beautify').html; 2 | const phpParser = require('php-parser'); 3 | const htmlparser = require("htmlparser2"); 4 | 5 | function getFormatOption(options, key, dflt) { 6 | if (options && Object.prototype.hasOwnProperty.call(options, key)) { 7 | let value = options[key]; 8 | if (value !== null) { 9 | return value; 10 | } 11 | } 12 | return dflt; 13 | } 14 | 15 | function getTagsFormatOption(options, key, dflt) { 16 | let list = getFormatOption(options, key, null); 17 | if (typeof list === 'string') { 18 | if (list.length > 0) { 19 | return list.split(',').map(t => t.trim().toLowerCase()); 20 | } 21 | return []; 22 | } 23 | return dflt; 24 | } 25 | 26 | /** 27 | * comment php code, ignore php code when formatting html 28 | * @param {php code} php 29 | */ 30 | function preAction(php) { 31 | let scriptStyleRanges = getScriptStyleRanges(php); 32 | let strArr = []; 33 | let tokens = (new phpParser()).tokenGetAll(php); 34 | let c = tokens.length; 35 | let index = 0; 36 | for (let i = 0; i < c; i++) { 37 | let t = tokens[i]; 38 | if (inScriptStyleTag(scriptStyleRanges, index)) { 39 | if (typeof (t) == 'object') { 40 | if (t[0] == 'T_OPEN_TAG' || t[0] == 'T_OPEN_TAG_WITH_ECHO') { 41 | strArr.push('/*%pcs-comment-start#' + t[1]); 42 | } else if (t[0] == 'T_CLOSE_TAG') { 43 | // fix new line issue 44 | let ms = t[1].match(/(\S+)(\s+)$/); 45 | if (ms) { 46 | strArr.push(ms[1] + '%pcs-comment-end#*/' + ms[2]); 47 | } else { 48 | strArr.push(t[1] + '%pcs-comment-end#*/'); 49 | } 50 | } else { 51 | if (t[0] == 'T_INLINE_HTML') { 52 | strArr.push(t[1]); 53 | } else { 54 | let str = t[1].replace(/\*\//g, '*%comment-end#/') 55 | .replace(/"/g, 'pcs%quote#1') 56 | .replace(/'/g, 'pcs%quote~2'); 57 | strArr.push(str); 58 | } 59 | } 60 | index += t[1].length; 61 | } else { 62 | strArr.push(t); 63 | index += t.length; 64 | } 65 | } else { 66 | if (typeof (t) == 'object') { 67 | if (t[0] == 'T_OPEN_TAG' || t[0] == 'T_OPEN_TAG_WITH_ECHO') { 68 | // ' + ms[2]); 75 | } else { 76 | strArr.push(t[1] + '%pcs-comment-end#-->'); 77 | } 78 | } else { 79 | if (t[0] == 'T_INLINE_HTML') { 80 | strArr.push(t[1]); 81 | } else { 82 | let str = t[1].replace(/-->/g, '-%comment-end#->') 83 | .replace(/"/g, 'pcs%quote#1') 84 | .replace(/'/g, 'pcs%quote~2'); 85 | strArr.push(str); 86 | } 87 | } 88 | index += t[1].length; 89 | } else { 90 | strArr.push(t); 91 | index += t.length; 92 | } 93 | } 94 | } 95 | if (typeof (tokens[c - 1]) == 'object' && (tokens[c - 1][0] != 'T_CLOSE_TAG' && tokens[c - 1][0] != 'T_INLINE_HTML')) { 96 | strArr.push('?>%pcs-comment-end#-->'); 97 | } 98 | return strArr.join(''); 99 | } 100 | 101 | /** 102 | * restore commented php code 103 | * @param {php code} php 104 | */ 105 | function afterAction(php) { 106 | return php.replace(/\?>\s*%pcs-comment-end#-->\s*$/g, '') 107 | .replace(/%pcs-comment-end#-->/g, '') 108 | .replace(/<\/i>\s*') 110 | .replace(/%pcs-comment-end#\*\//g, '') 111 | .replace(/\/\*%pcs-comment-start#/g, '') 112 | .replace(/\*%comment-end#\//g, '*/') 113 | .replace(/pcs%quote#1/g, '"') 114 | .replace(/pcs%quote~2/g, "'"); 115 | } 116 | 117 | /** 118 | * get all script/style tag ranges 119 | * @param {php code} php 120 | */ 121 | function getScriptStyleRanges(php) { 122 | let ranges = []; 123 | let start = 0; 124 | let parser = new htmlparser.Parser({ 125 | onopentag: (name) => { 126 | if (name === "script" || name === 'style') { 127 | start = parser.startIndex; 128 | } 129 | }, 130 | onclosetag: (name) => { 131 | if (name === "script" || name === 'style') { 132 | ranges.push([start, parser.endIndex]); 133 | } 134 | }, 135 | }, { 136 | decodeEntities: true, 137 | }); 138 | parser.write(php); 139 | parser.end(); 140 | return ranges; 141 | } 142 | 143 | /** 144 | * check current index wheather in script/style tag 145 | * @param {Array} ranges 146 | * @param {int} index 147 | */ 148 | function inScriptStyleTag(ranges, index) { 149 | for (let i = 0, c = ranges.length; i < c; i++) { 150 | if (index >= ranges[i][0] && index <= ranges[i][1]) { 151 | return true; 152 | } 153 | } 154 | return false; 155 | } 156 | 157 | /** 158 | * @param {string} text 159 | */ 160 | exports.format = (text, options) => { 161 | //if only php code, return text directly 162 | let indexOfPhp = text.indexOf(' -1 && indexOfPhp == text.lastIndexOf('') == text.lastIndexOf('?>')) { 164 | return text.replace(/^\s+<\?php/i, 'F1 and select `Extensions: Install Extension`, then search for PHP CS Fixer. 8 | 9 | ## Usage 10 | 11 | F1 -> `php-cs-fixer: fix this file` 12 | 13 | or keyboard shortcut `alt+shift+f` vs code default formatter shortcut 14 | 15 | or right mouse context menu `Format Document` 16 | 17 | or right mouse context menu `Format Selection` 18 | 19 | or right mouse context menu on explorer `php-cs-fixer: fix` 20 | 21 | ## Install php-cs-fixer 22 | 23 | 1. this extension has included `php-cs-fixer.phar` for beginner, maybe performance lower. 24 | 25 | 2. if you want to install php-cs-fixer by yourself, see: [php-cs-fixer Installation guide](https://github.com/FriendsOfPHP/PHP-CS-Fixer#installation) 26 | 27 | ## Configuration 28 | 29 | ```JSON 30 | { 31 | "php-cs-fixer.executablePath": "php-cs-fixer", 32 | "php-cs-fixer.executablePathWindows": "", //eg: php-cs-fixer.bat 33 | "php-cs-fixer.onsave": false, 34 | "php-cs-fixer.rules": "@PSR2", 35 | "php-cs-fixer.config": ".php_cs;.php_cs.dist", 36 | "php-cs-fixer.allowRisky": false, 37 | "php-cs-fixer.pathMode": "override", 38 | "php-cs-fixer.exclude": [], 39 | "php-cs-fixer.autoFixByBracket": true, 40 | "php-cs-fixer.autoFixBySemicolon": false, 41 | "php-cs-fixer.formatHtml": false, 42 | "php-cs-fixer.documentFormattingProvider": true 43 | } 44 | ``` 45 | 46 | install php-cs-fixer by composer 47 | 48 | ```JSON 49 | "php-cs-fixer.executablePath": "php-cs-fixer" 50 | ``` 51 | 52 | **TIP:** try "php-cs-fixer.bat" on **Windows**. 53 | 54 | or use phar file 55 | 56 | ```JSON 57 | "php-cs-fixer.executablePath: "/full/path/of/php-cs-fixer.phar" 58 | ``` 59 | 60 | You also have `executablePathWindows` available if you want to specify Windows specific path. Useful if you share your workspace settings among different environments. 61 | 62 | executablePath can use ${workspaceFolder} as workspace first root folder path. 63 | 64 | [executablePath, executablePathWindows, config] can use "~/" as user home directory on os. 65 | 66 | 67 | Additionally you can configure this extension to execute on save. 68 | 69 | ```JSON 70 | "php-cs-fixer.onsave": true 71 | ``` 72 | 73 | you can format html at the same time. 74 | 75 | ```JSON 76 | "php-cs-fixer.formatHtml": true 77 | ``` 78 | 79 | You can use a config file form a list of semicolon separated values 80 | 81 | ```JSON 82 | "php-cs-fixer.config": ".php_cs;.php_cs.dist" 83 | ``` 84 | 85 | config file can place in workspace root folder or .vscode folder or any other folders: 86 | 87 | ```JSON 88 | "php-cs-fixer.config": "/full/config/file/path" 89 | ``` 90 | 91 | Relative paths are only considered when a workspace folder is open. 92 | 93 | config file .php_cs example 94 | 95 | ```php 96 | setRules(array( 100 | '@PSR2' => true, 101 | 'array_indentation' => true, 102 | 'array_syntax' => array('syntax' => 'short'), 103 | 'combine_consecutive_unsets' => true, 104 | 'method_separation' => true, 105 | 'no_multiline_whitespace_before_semicolons' => true, 106 | 'single_quote' => true, 107 | 108 | 'binary_operator_spaces' => array( 109 | 'align_double_arrow' => false, 110 | 'align_equals' => false, 111 | ), 112 | // 'blank_line_after_opening_tag' => true, 113 | // 'blank_line_before_return' => true, 114 | 'braces' => array( 115 | 'allow_single_line_closure' => true, 116 | ), 117 | // 'cast_spaces' => true, 118 | // 'class_definition' => array('singleLine' => true), 119 | 'concat_space' => array('spacing' => 'one'), 120 | 'declare_equal_normalize' => true, 121 | 'function_typehint_space' => true, 122 | 'hash_to_slash_comment' => true, 123 | 'include' => true, 124 | 'lowercase_cast' => true, 125 | // 'native_function_casing' => true, 126 | // 'new_with_braces' => true, 127 | // 'no_blank_lines_after_class_opening' => true, 128 | // 'no_blank_lines_after_phpdoc' => true, 129 | // 'no_empty_comment' => true, 130 | // 'no_empty_phpdoc' => true, 131 | // 'no_empty_statement' => true, 132 | 'no_extra_consecutive_blank_lines' => array( 133 | 'curly_brace_block', 134 | 'extra', 135 | 'parenthesis_brace_block', 136 | 'square_brace_block', 137 | 'throw', 138 | 'use', 139 | ), 140 | // 'no_leading_import_slash' => true, 141 | // 'no_leading_namespace_whitespace' => true, 142 | // 'no_mixed_echo_print' => array('use' => 'echo'), 143 | 'no_multiline_whitespace_around_double_arrow' => true, 144 | // 'no_short_bool_cast' => true, 145 | // 'no_singleline_whitespace_before_semicolons' => true, 146 | 'no_spaces_around_offset' => true, 147 | // 'no_trailing_comma_in_list_call' => true, 148 | // 'no_trailing_comma_in_singleline_array' => true, 149 | // 'no_unneeded_control_parentheses' => true, 150 | // 'no_unused_imports' => true, 151 | 'no_whitespace_before_comma_in_array' => true, 152 | 'no_whitespace_in_blank_line' => true, 153 | // 'normalize_index_brace' => true, 154 | 'object_operator_without_whitespace' => true, 155 | // 'php_unit_fqcn_annotation' => true, 156 | // 'phpdoc_align' => true, 157 | // 'phpdoc_annotation_without_dot' => true, 158 | // 'phpdoc_indent' => true, 159 | // 'phpdoc_inline_tag' => true, 160 | // 'phpdoc_no_access' => true, 161 | // 'phpdoc_no_alias_tag' => true, 162 | // 'phpdoc_no_empty_return' => true, 163 | // 'phpdoc_no_package' => true, 164 | // 'phpdoc_no_useless_inheritdoc' => true, 165 | // 'phpdoc_return_self_reference' => true, 166 | // 'phpdoc_scalar' => true, 167 | // 'phpdoc_separation' => true, 168 | // 'phpdoc_single_line_var_spacing' => true, 169 | // 'phpdoc_summary' => true, 170 | // 'phpdoc_to_comment' => true, 171 | // 'phpdoc_trim' => true, 172 | // 'phpdoc_types' => true, 173 | // 'phpdoc_var_without_name' => true, 174 | // 'pre_increment' => true, 175 | // 'return_type_declaration' => true, 176 | // 'self_accessor' => true, 177 | // 'short_scalar_cast' => true, 178 | 'single_blank_line_before_namespace' => true, 179 | // 'single_class_element_per_statement' => true, 180 | // 'space_after_semicolon' => true, 181 | // 'standardize_not_equals' => true, 182 | 'ternary_operator_spaces' => true, 183 | // 'trailing_comma_in_multiline_array' => true, 184 | 'trim_array_spaces' => true, 185 | 'unary_operator_spaces' => true, 186 | 'whitespace_after_comma_in_array' => true, 187 | )) 188 | //->setIndent("\t") 189 | ->setLineEnding("\n") 190 | ; 191 | ``` 192 | 193 | ## Auto fix 194 | 195 | 1. by Bracket, when press down the key } auto fix the code in the brackets {} 196 | 2. by Semicolon, when press down the key ; auto fix the code at the current line 197 | 198 | For more information please visit: [https://github.com/FriendsOfPHP/PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) 199 | 200 | ## License 201 | 202 | MIT 203 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const vscode = require('vscode') 3 | const { commands, workspace, window, languages, Range, Position } = vscode 4 | const fs = require('fs') 5 | const os = require('os') 6 | const cp = require('child_process') 7 | const path = require('path') 8 | const beautifyHtml = require('./beautifyHtml') 9 | const anymatch = require('anymatch') 10 | const TmpDir = os.tmpdir() 11 | let isRunning = false, outputChannel, statusBarItem, lastActiveEditor, statusBarTimer 12 | 13 | class PHPCSFixer { 14 | constructor() { 15 | this.loadSettings() 16 | this.checkUpdate() 17 | } 18 | 19 | loadSettings() { 20 | let config = workspace.getConfiguration('php-cs-fixer') 21 | this.onsave = config.get('onsave', false) 22 | this.autoFixByBracket = config.get('autoFixByBracket', true) 23 | this.autoFixBySemicolon = config.get('autoFixBySemicolon', false) 24 | this.executablePath = config.get('executablePath', process.platform === "win32" ? 'php-cs-fixer.bat' : 'php-cs-fixer') 25 | if (process.platform == "win32" && config.has('executablePathWindows') && config.get('executablePathWindows').length > 0) { 26 | this.executablePath = config.get('executablePathWindows') 27 | } 28 | this.executablePath = this.executablePath.replace('${extensionPath}', __dirname) 29 | this.executablePath = this.executablePath.replace(/^~\//, os.homedir() + '/') 30 | this.rules = config.get('rules', '@PSR2') 31 | if (typeof (this.rules) == 'object') { 32 | this.rules = JSON.stringify(this.rules) 33 | } 34 | this.config = config.get('config', '.php_cs;.php_cs.dist') 35 | this.formatHtml = config.get('formatHtml', false) 36 | this.documentFormattingProvider = config.get('documentFormattingProvider', true) 37 | this.allowRisky = config.get('allowRisky', false) 38 | this.pathMode = config.get('pathMode', 'override') 39 | this.exclude = config.get('exclude', []) 40 | this.showOutput = config.get('showOutput', true) 41 | 42 | if (this.executablePath.endsWith(".phar")) { 43 | this.pharPath = this.executablePath.replace(/^php[^ ]* /i, '') 44 | this.executablePath = workspace.getConfiguration('php').get('validate.executablePath', 'php') 45 | if (this.executablePath == null || this.executablePath.length == 0) { 46 | this.executablePath = 'php' 47 | } 48 | } else { 49 | this.pharPath = null 50 | } 51 | 52 | //if editor.formatOnSave=true, change timeout to 5000 53 | var editorConfig = workspace.getConfiguration('editor', null) 54 | this.editorFormatOnSave = editorConfig.get('formatOnSave') 55 | if (this.editorFormatOnSave) { 56 | let timeout = editorConfig.get('formatOnSaveTimeout') 57 | if (timeout == 750 || timeout == 1250) { 58 | editorConfig.update('formatOnSaveTimeout', 5000, true) 59 | } 60 | } 61 | this.fileAutoSave = workspace.getConfiguration('files', null).get('autoSave') 62 | this.fileAutoSaveDelay = workspace.getConfiguration('files', null).get('autoSaveDelay', 1000) 63 | } 64 | 65 | getActiveWorkspacePath() { 66 | let folder = workspace.getWorkspaceFolder(window.activeTextEditor.document.uri) 67 | if (folder != undefined) { 68 | return folder.uri.fsPath 69 | } 70 | return undefined 71 | } 72 | 73 | getArgs(fileName) { 74 | if (workspace.workspaceFolders != undefined) { 75 | // ${workspaceRoot} is depricated 76 | const pattern = /^\$\{workspace(Root|Folder)\}/; 77 | this.realExecutablePath = this.executablePath.replace(pattern, this.getActiveWorkspacePath() || workspace.workspaceFolders[0].uri.fsPath) 78 | } else 79 | this.realExecutablePath = undefined 80 | 81 | let args = ['fix', '--using-cache=no', fileName] 82 | if (this.pharPath != null) { 83 | args.unshift(this.pharPath) 84 | } 85 | let useConfig = false 86 | if (this.config.length > 0) { 87 | let rootPath = this.getActiveWorkspacePath() 88 | let configFiles = this.config.split(';') // allow multiple files definitions semicolon separated values 89 | .filter(file => '' !== file) // do not include empty definitions 90 | .map(file => file.replace(/^~\//, os.homedir() + '/')) // replace ~/ with home dir 91 | 92 | // include also {workspace.rootPath}/.vscode/ & {workspace.rootPath}/ 93 | let searchPaths = [] 94 | if (rootPath !== undefined) { 95 | searchPaths = [ 96 | rootPath + '/.vscode/', 97 | rootPath + '/', 98 | ] 99 | } 100 | 101 | const files = [] 102 | for (const file of configFiles) { 103 | if (path.isAbsolute(file)) { 104 | files.push(file) 105 | } else { 106 | for (const searchPath of searchPaths) { 107 | files.push(searchPath + file) 108 | } 109 | } 110 | } 111 | 112 | for (let i = 0, len = files.length; i < len; i++) { 113 | let c = files[i] 114 | if (fs.existsSync(c)) { 115 | args.push('--config=' + c) 116 | useConfig = true 117 | break 118 | } 119 | } 120 | } 121 | if (!useConfig && this.rules) { 122 | args.push('--rules=' + this.rules) 123 | } 124 | if (this.allowRisky) { 125 | args.push('--allow-risky=yes') 126 | } 127 | if (fileName.startsWith(TmpDir + '/temp-')) { 128 | args.push('--path-mode=override') 129 | } else { 130 | args.push('--path-mode=' + this.pathMode) 131 | } 132 | 133 | console.log(args) 134 | return args 135 | } 136 | 137 | format(text, isDiff, workingDirectory = null, isPartial = false) { 138 | isDiff = isDiff ? true : false 139 | isRunning = true 140 | 141 | this.statusBar("php-cs-fixer: formatting") 142 | 143 | let filePath = TmpDir + window.activeTextEditor.document.uri.fsPath.replace(/^.*[\\/]/, '/') 144 | // if interval between two operations too short, see: https://github.com/junstyle/vscode-php-cs-fixer/issues/76 145 | // so set different filePath for partial codes; 146 | if (isPartial) { 147 | filePath = TmpDir + "/php-cs-fixer-partial.php" 148 | } 149 | 150 | fs.writeFileSync(filePath, text) 151 | 152 | const opts = {} 153 | if (workingDirectory !== null) { 154 | opts.cwd = workingDirectory 155 | } 156 | 157 | let args = this.getArgs(filePath) 158 | let exec = cp.spawn(this.realExecutablePath || this.executablePath, args, opts) 159 | 160 | let promise = new Promise((resolve, reject) => { 161 | exec.on("error", err => { 162 | reject(err) 163 | isRunning = false 164 | if (err.code == 'ENOENT') { 165 | this.errorTip() 166 | } 167 | }) 168 | exec.on("exit", code => { 169 | if (code == 0) { 170 | if (isDiff) { 171 | resolve(filePath) 172 | } else { 173 | try { 174 | let fixed = fs.readFileSync(filePath, 'utf-8') 175 | if (fixed.length > 0) { 176 | resolve(fixed) 177 | } else { 178 | reject() 179 | } 180 | } catch (err) { 181 | reject(err) 182 | } 183 | } 184 | } else { 185 | let msgs = { 186 | 1: 'PHP CS Fixer: php general error.', 187 | 16: 'PHP CS Fixer: Configuration error of the application.', // The path "/file/path.php" is not readable 188 | 32: 'PHP CS Fixer: Configuration error of a Fixer.', 189 | 64: 'PHP CS Fixer: Exception raised within the application.', 190 | } 191 | if (code != 16) 192 | window.showErrorMessage(msgs[code]) 193 | reject(msgs[code]) 194 | } 195 | 196 | if (!isDiff) { 197 | fs.unlink(filePath, function (err) { }) 198 | } 199 | isRunning = false 200 | this.statusBar("php-cs-fixer: finished", 1000) 201 | }) 202 | }) 203 | 204 | exec.stdout.on('data', buffer => { 205 | console.log(buffer.toString()) 206 | }) 207 | exec.stderr.on('data', buffer => { 208 | console.error(buffer.toString()) 209 | if (buffer.toString().includes('Files that were not fixed due to errors reported during linting before fixing:')) { 210 | this.statusBar("php-cs-fixer: php syntax error", 30000) 211 | } 212 | }) 213 | 214 | return promise 215 | } 216 | 217 | fix(filePath) { 218 | isRunning = true 219 | this.output(true) 220 | this.statusBar("php-cs-fixer: fixing") 221 | 222 | const opts = {} 223 | 224 | if (filePath != '') { 225 | opts.cwd = path.dirname(filePath) 226 | } 227 | 228 | let args = this.getArgs(filePath) 229 | let exec = cp.spawn(this.realExecutablePath || this.executablePath, args, opts) 230 | 231 | exec.on("error", err => { 232 | this.output(err) 233 | if (err.code == 'ENOENT') { 234 | isRunning = false 235 | this.errorTip() 236 | } 237 | }) 238 | exec.on("exit", code => { 239 | isRunning = false 240 | this.statusBar("php-cs-fixer: finished", 1000) 241 | }) 242 | 243 | exec.stdout.on('data', buffer => { 244 | this.output(buffer.toString()) 245 | }) 246 | exec.stderr.on('data', buffer => { 247 | this.output(buffer.toString()) 248 | }) 249 | exec.on('close', code => { 250 | // console.log(code); 251 | }) 252 | } 253 | 254 | diff(filePath) { 255 | this.format(fs.readFileSync(filePath), true, path.dirname(filePath)).then((tempFilePath) => { 256 | commands.executeCommand('vscode.diff', vscode.Uri.file(filePath), vscode.Uri.file(tempFilePath), 'diff') 257 | }).catch(err => { 258 | console.log(err) 259 | }) 260 | } 261 | 262 | output(str) { 263 | if (!this.showOutput) return 264 | if (outputChannel == null) { 265 | outputChannel = window.createOutputChannel('php-cs-fixer') 266 | } 267 | if (str === true) { 268 | outputChannel.clear() 269 | outputChannel.show(true) 270 | return 271 | } 272 | outputChannel.appendLine(str) 273 | } 274 | 275 | statusBar(str, disappear = 0) { 276 | clearTimeout(statusBarTimer) 277 | if (statusBarItem == null) { 278 | statusBarItem = window.createStatusBarItem(vscode.StatusBarAlignment.Left, -10000000) 279 | // statusBarItem.command = 'toggleOutput'; 280 | statusBarItem.tooltip = 'php-cs-fixer' 281 | } 282 | if (str === false) { 283 | statusBarItem.hide() 284 | return 285 | } else if (str === true) { 286 | statusBarItem.show() 287 | return 288 | } 289 | statusBarItem.show() 290 | statusBarItem.text = str 291 | if (disappear > 0) 292 | statusBarTimer = setTimeout(() => statusBarItem.hide(), disappear) 293 | } 294 | 295 | doAutoFixByBracket(event) { 296 | if (event.contentChanges.length == 0) return 297 | let pressedKey = event.contentChanges[0].text 298 | // console.log(pressedKey); 299 | if (!/^\s*\}$/.test(pressedKey)) { 300 | return 301 | } 302 | 303 | let editor = window.activeTextEditor 304 | let document = editor.document 305 | let originalStart = editor.selection.start 306 | commands.executeCommand("editor.action.jumpToBracket").then(() => { 307 | let start = editor.selection.start 308 | let offsetStart0 = document.offsetAt(originalStart) 309 | let offsetStart1 = document.offsetAt(start) 310 | if (offsetStart0 == offsetStart1) { 311 | return 312 | } 313 | 314 | let nextChar = document.getText(new Range(start, start.translate(0, 1))) 315 | if (offsetStart0 - offsetStart1 < 3 || nextChar != '{') { 316 | // jumpToBracket to wrong match bracket, do nothing 317 | commands.executeCommand("cursorUndo") 318 | return 319 | } 320 | 321 | let line = document.lineAt(start) 322 | let code = " fixed.replace(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\r?\n/, '').replace(/\s*$/, '') 324 | let searchIndex = -1 325 | if (/^\s*\{\s*$/.test(line.text)) { 326 | // check previous line 327 | let preline = document.lineAt(line.lineNumber - 1) 328 | searchIndex = preline.text.search(/((if|for|foreach|while|switch|^\s*function\s+\w+|^\s*function\s*)\s*\(.+?\)|(class|trait|interface)\s+[\w ]+|do|try)\s*$/i) 329 | if (searchIndex > -1) { 330 | line = preline 331 | } 332 | } else { 333 | searchIndex = line.text.search(/((if|for|foreach|while|switch|^\s*function\s+\w+|^\s*function\s*)\s*\(.+?\)|(class|trait|interface)\s+[\w ]+|do|try)\s*\{\s*$/i) 334 | } 335 | 336 | if (searchIndex > -1) { 337 | start = line.range.start 338 | } else { 339 | // indent + if(1) 340 | code += line.text.match(/^(\s*)\S+/)[1] + "if(1)" 341 | dealFun = fixed => { 342 | let match = fixed.match(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\s+?if\s*\(\s*1\s*\)\s*(\{[\s\S]+?\})\s*$/i) 343 | if (match != null) { 344 | fixed = match[1] 345 | } else { 346 | fixed = '' 347 | } 348 | return fixed 349 | } 350 | } 351 | 352 | commands.executeCommand("cursorUndo").then(() => { 353 | let end = editor.selection.start 354 | let range = new Range(start, end) 355 | let originalText = code + document.getText(range) 356 | 357 | let workingDirectory = null 358 | if (document.uri.scheme == 'file') { 359 | workingDirectory = path.dirname(document.uri.fsPath) 360 | } 361 | this.format(originalText, false, workingDirectory, true).then((text) => { 362 | if (text != originalText) { 363 | text = dealFun(text) 364 | editor.edit((builder) => { 365 | builder.replace(range, text) 366 | }).then(() => { 367 | if (editor.selections.length > 0) { 368 | commands.executeCommand("cancelSelection") 369 | } 370 | }) 371 | } 372 | }).catch(err => { 373 | console.log(err) 374 | }) 375 | }) 376 | }) 377 | } 378 | 379 | doAutoFixBySemicolon(event) { 380 | if (event.contentChanges.length == 0) return 381 | let pressedKey = event.contentChanges[0].text 382 | // console.log(pressedKey); 383 | if (pressedKey != ';') { 384 | return 385 | } 386 | let editor = window.activeTextEditor 387 | let line = editor.document.lineAt(editor.selection.start) 388 | if (line.text.length < 5) { 389 | return 390 | } 391 | 392 | let dealFun = fixed => fixed.replace(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\r?\n/, '').replace(/\s+$/, '') 393 | let range = line.range 394 | let originalText = ' { 401 | if (text != originalText) { 402 | text = dealFun(text) 403 | editor.edit((builder) => { 404 | builder.replace(range, text) 405 | }).then(() => { 406 | if (editor.selections.length > 0) { 407 | commands.executeCommand("cancelSelection") 408 | } 409 | }) 410 | } 411 | }).catch(err => { 412 | console.log(err) 413 | }) 414 | } 415 | 416 | registerDocumentProvider(document, options) { 417 | if (this.isExcluded(document)) { 418 | return 419 | } 420 | 421 | // only activeTextEditor, or last activeTextEditor 422 | if (window.activeTextEditor == undefined 423 | || (window.activeTextEditor.document.uri.toString() != document.uri.toString() && lastActiveEditor != document.uri.toString())) 424 | return 425 | 426 | isRunning = false 427 | return new Promise((resolve, reject) => { 428 | let originalText = document.getText() 429 | let lastLine = document.lineAt(document.lineCount - 1) 430 | let range = new Range(new Position(0, 0), lastLine.range.end) 431 | let htmlOptions = Object.assign(options, workspace.getConfiguration('html').get('format')) 432 | let originalText2 = this.formatHtml ? beautifyHtml.format(originalText, htmlOptions) : originalText 433 | 434 | let workingDirectory = null 435 | if (document.uri.scheme == 'file') { 436 | workingDirectory = path.dirname(document.uri.fsPath) 437 | } 438 | this.format(originalText2, false, workingDirectory).then((text) => { 439 | if (text != originalText) { 440 | resolve([new vscode.TextEdit(range, text)]) 441 | } else { 442 | resolve() 443 | } 444 | }).catch(err => { 445 | console.log(err) 446 | reject() 447 | }) 448 | }) 449 | } 450 | 451 | registerDocumentRangeProvider(document, range) { 452 | if (this.isExcluded(document)) { 453 | return 454 | } 455 | 456 | // only activeTextEditor, or last activeTextEditor 457 | if (window.activeTextEditor == undefined 458 | || (window.activeTextEditor.document.uri.toString() != document.uri.toString() && lastActiveEditor != document.uri.toString())) 459 | return 460 | 461 | isRunning = false 462 | return new Promise((resolve, reject) => { 463 | let originalText = document.getText(range) 464 | if (originalText.replace(/\s+/g, '').length == 0) { 465 | reject() 466 | return 467 | } 468 | let addPHPTag = false 469 | if (originalText.search(/^\s*<\?php/i) == -1) { 470 | originalText = " { 479 | if (addPHPTag) { 480 | text = text.replace(/^<\?php\r?\n/, '') 481 | } 482 | if (text != originalText) { 483 | resolve([new vscode.TextEdit(range, text)]) 484 | } else { 485 | resolve() 486 | } 487 | }).catch(err => { 488 | console.log(err) 489 | reject() 490 | }) 491 | }) 492 | } 493 | 494 | isExcluded(document) { 495 | if (this.exclude.length > 0 && document.uri.scheme == 'file' && !document.isUntitled) { 496 | return anymatch(this.exclude, document.uri.path) 497 | } 498 | return false 499 | } 500 | 501 | errorTip() { 502 | // window.showErrorMessage('PHP CS Fixer: ' + err.message + ". executablePath not found. "); 503 | window.showErrorMessage('PHP CS Fixer: executablePath not found, please check your settings. It will set to built-in php-cs-fixer.phar. Try again!', 'OK') 504 | let config = workspace.getConfiguration('php-cs-fixer') 505 | config.update('executablePath', '${extensionPath}/php-cs-fixer.phar', true) 506 | } 507 | 508 | checkUpdate() { 509 | setTimeout(() => { 510 | let config = workspace.getConfiguration('php-cs-fixer') 511 | let executablePath = config.get('executablePath', 'php-cs-fixer') 512 | let lastDownload = config.get('lastDownload', 1) 513 | if (lastDownload !== 0 && executablePath == '${extensionPath}/php-cs-fixer.phar' && lastDownload + 1000 * 3600 * 24 * 10 < (new Date()).getTime()) { 514 | console.log('php-cs-fixer: check for updating...') 515 | const { DownloaderHelper } = require('node-downloader-helper') 516 | let dl = new DownloaderHelper('https://cs.symfony.com/download/php-cs-fixer-v2.phar', __dirname, { 'fileName': 'php-cs-fixer.phar', 'override': true }) 517 | dl.on('end', () => config.update('lastDownload', (new Date()).getTime(), true)) 518 | dl.start() 519 | } 520 | }, 1000 * 60) 521 | } 522 | } 523 | 524 | exports.activate = context => { 525 | let pcf = new PHPCSFixer() 526 | 527 | context.subscriptions.push(window.onDidChangeActiveTextEditor(te => { 528 | if (pcf.fileAutoSave != 'off') { 529 | setTimeout(() => lastActiveEditor = te == undefined ? undefined : te.document.uri.toString(), pcf.fileAutoSaveDelay + 100) 530 | } 531 | })) 532 | 533 | context.subscriptions.push(workspace.onWillSaveTextDocument((event) => { 534 | if (event.document.languageId == 'php' && pcf.onsave && pcf.editorFormatOnSave == false) { 535 | event.waitUntil(commands.executeCommand("editor.action.formatDocument")) 536 | } 537 | })) 538 | 539 | context.subscriptions.push(commands.registerTextEditorCommand('php-cs-fixer.fix', (textEditor) => { 540 | if (textEditor.document.languageId == 'php') { 541 | commands.executeCommand("editor.action.formatDocument") 542 | } 543 | })) 544 | 545 | context.subscriptions.push(workspace.onDidChangeTextDocument((event) => { 546 | if (event.document.languageId == 'php' && isRunning == false) { 547 | if (pcf.isExcluded(event.document)) { 548 | return 549 | } 550 | 551 | if (pcf.autoFixByBracket) { 552 | pcf.doAutoFixByBracket(event) 553 | } 554 | if (pcf.autoFixBySemicolon) { 555 | pcf.doAutoFixBySemicolon(event) 556 | } 557 | } 558 | })) 559 | 560 | context.subscriptions.push(workspace.onDidChangeConfiguration(() => { 561 | pcf.loadSettings() 562 | })) 563 | 564 | if (pcf.documentFormattingProvider) { 565 | context.subscriptions.push(languages.registerDocumentFormattingEditProvider('php', { 566 | provideDocumentFormattingEdits: (document, options, token) => { 567 | return pcf.registerDocumentProvider(document, options) 568 | }, 569 | })) 570 | 571 | context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider('php', { 572 | provideDocumentRangeFormattingEdits: (document, range, options, token) => { 573 | return pcf.registerDocumentRangeProvider(document, range) 574 | }, 575 | })) 576 | } 577 | 578 | context.subscriptions.push(commands.registerCommand('php-cs-fixer.fix2', (f) => { 579 | if (f == undefined) { 580 | let editor = window.activeTextEditor 581 | if (editor != undefined && editor.document.languageId == 'php') { 582 | f = editor.document.uri 583 | } 584 | } 585 | if (f != undefined) { 586 | pcf.fix(f.fsPath) 587 | } else { 588 | // only run fix command, not provide file path 589 | pcf.fix('') 590 | } 591 | })) 592 | 593 | context.subscriptions.push(commands.registerCommand('php-cs-fixer.diff', (f) => { 594 | if (f == undefined) { 595 | let editor = window.activeTextEditor 596 | if (editor != undefined && editor.document.languageId == 'php') { 597 | f = editor.document.uri 598 | } 599 | } 600 | if (f != undefined) { 601 | pcf.diff(f.fsPath) 602 | } 603 | })) 604 | 605 | } 606 | 607 | exports.deactivate = () => { 608 | if (outputChannel) { 609 | outputChannel.clear() 610 | outputChannel.dispose() 611 | } 612 | if (statusBarItem) { 613 | statusBarItem.hide() 614 | statusBarItem.dispose() 615 | } 616 | outputChannel = null 617 | statusBarItem = null 618 | } 619 | --------------------------------------------------------------------------------