├── logo.png ├── .gitignore ├── .vscodeignore ├── tsconfig.json ├── .vscode ├── tasks.json └── launch.json ├── download-phar.js ├── src ├── index.d.ts ├── output.ts ├── runAsync.ts ├── beautifyHtml.ts └── index.ts ├── .github └── workflows │ └── publish.yml ├── LICENSE.txt ├── package.json └── README.md /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junstyle/vscode-php-cs-fixer/HEAD/logo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslint* 3 | php-cs-fixer.phar 4 | *.vsix 5 | extension_pack.js 6 | extension_pack.js.map 7 | *.js.map 8 | tests 9 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .github/** 3 | .gitignore 4 | .eslintrc.json 5 | node_modules/ 6 | src/ 7 | jsconfig.json 8 | tsconfig.json 9 | download-phar.js 10 | package-lock.json 11 | pnpm-lock.yaml 12 | yarn.lock -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "lib": ["es2020"], 6 | "outDir": "out", 7 | "rootDir": "src", 8 | "sourceMap": true 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", ".vscode-test"] 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "watch", 7 | "group": "build", 8 | "problemMatcher": "$esbuild-watch", 9 | "isBackground": true, 10 | "label": "npm: watch" 11 | }, 12 | { 13 | "type": "npm", 14 | "script": "build", 15 | "group": "build", 16 | "problemMatcher": "$esbuild", 17 | "label": "npm: build" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /download-phar.js: -------------------------------------------------------------------------------- 1 | const { DownloaderHelper } = require('node-downloader-helper') 2 | const dl = new DownloaderHelper('https://cs.symfony.com/download/php-cs-fixer-v3.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() 9 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export class PHPCSFixerConfig { 2 | onsave: boolean 3 | autoFixByBracket: boolean 4 | autoFixBySemicolon: boolean 5 | executablePath: string 6 | rules: string | Object 7 | config: string 8 | formatHtml: boolean 9 | documentFormattingProvider: boolean 10 | allowRisky: boolean 11 | pathMode: 'pathMode' | 'override' 12 | ignorePHPVersion: boolean 13 | exclude: string[] 14 | pharPath: string 15 | editorFormatOnSave: boolean 16 | // fileAutoSave: boolean 17 | // fileAutoSaveDelay: number 18 | tmpDir: string 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | branches: ['master'] 6 | # pull_request: 7 | # branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | # cache: 'npm' 22 | 23 | - run: npm install 24 | - run: npm install -g vsce esbuild 25 | - run: node ./download-phar.js 26 | # - run: esbuild ./src/index.ts --bundle --minify --outfile=./index.js --external:vscode --format=cjs --platform=node 27 | - run: vsce publish -p ${{ secrets.VSC_TOKEN }} 28 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/**/*.js"], 11 | "preLaunchTask": "npm: watch" 12 | }, 13 | { 14 | "name": "Extension Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"], 18 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 19 | "preLaunchTask": "${defaultBuildTask}" 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 junstyle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/output.ts: -------------------------------------------------------------------------------- 1 | import { window, StatusBarAlignment } from 'vscode' 2 | 3 | let outputChannel = null 4 | let statusBarItem = null 5 | 6 | function createOutput() { 7 | if (outputChannel == null) { 8 | outputChannel = window.createOutputChannel('php-cs-fixer') 9 | } 10 | } 11 | 12 | export function output(str) { 13 | createOutput() 14 | outputChannel.appendLine(str) 15 | } 16 | 17 | export function showOutput() { 18 | createOutput() 19 | outputChannel.show(true) 20 | } 21 | 22 | export function clearOutput() { 23 | outputChannel?.clear() 24 | } 25 | 26 | export function statusInfo(str) { 27 | if (statusBarItem == null) { 28 | statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left, -10000000) 29 | statusBarItem.command = 'php-cs-fixer.showOutput' 30 | statusBarItem.tooltip = 'php-cs-fixer: show output' 31 | } 32 | 33 | statusBarItem.show() 34 | statusBarItem.text = 'php-cs-fixer: ' + str 35 | } 36 | 37 | export function hideStatusBar() { 38 | statusBarItem?.hide() 39 | } 40 | 41 | export function disposeOutput() { 42 | if (outputChannel) { 43 | outputChannel.clear() 44 | outputChannel.dispose() 45 | } 46 | if (statusBarItem) { 47 | statusBarItem.hide() 48 | statusBarItem.dispose() 49 | } 50 | outputChannel = null 51 | statusBarItem = null 52 | } 53 | -------------------------------------------------------------------------------- /src/runAsync.ts: -------------------------------------------------------------------------------- 1 | import { spawn, SpawnOptionsWithoutStdio } from 'child_process'; 2 | import { output } from './output'; 3 | 4 | export function runAsync(command: string, args: string[], options: SpawnOptionsWithoutStdio, onData: (data: Buffer) => void = null) { 5 | const cpOptions = Object.assign({}, options, { shell: process.platform == 'win32' }) 6 | let cp; 7 | try { 8 | if (process.platform == 'win32') { 9 | if (command.includes(" ") && command[0] != '"') { 10 | command = '"' + command + '"' 11 | } 12 | } 13 | 14 | output('runAsync: spawn ' + command); 15 | output(JSON.stringify(args, null, 2)) 16 | output(JSON.stringify(cpOptions, null, 2)) 17 | 18 | cp = spawn(command, args, cpOptions) 19 | } catch (err) { 20 | const promise = new Promise((resolve, reject) => { 21 | output('runAsync: error') 22 | output(JSON.stringify(err, null, 2)) 23 | output('runAsync: reject promise') 24 | reject(err) 25 | }) 26 | ; (promise as any).cp = cp 27 | 28 | return promise 29 | } 30 | 31 | const promise = new Promise((resolve, reject) => { 32 | let stdout = null 33 | let stderr = null 34 | 35 | cp.stdout && 36 | cp.stdout.on('data', (data) => { 37 | stdout = stdout || Buffer.from('') 38 | stdout = Buffer.concat([stdout, data]) 39 | onData && onData(data) 40 | }) 41 | 42 | cp.stderr && 43 | cp.stderr.on('data', (data) => { 44 | stderr = stderr || Buffer.from('') 45 | stderr = Buffer.concat([stderr, data]) 46 | // stdout = stdout || Buffer.from(''); 47 | // stdout = Buffer.concat([stdout, data]); 48 | onData && onData(data) 49 | }) 50 | 51 | const cleanupListeners = () => { 52 | cp.removeListener('error', onError) 53 | cp.removeListener('close', onClose) 54 | } 55 | 56 | const onError = (err) => { 57 | cleanupListeners() 58 | 59 | output('runAsync: error') 60 | output(JSON.stringify(err, null, 2)) 61 | output('runAsync: reject promise') 62 | reject(err) 63 | } 64 | 65 | const onClose = (code) => { 66 | cleanupListeners() 67 | 68 | const resolved = resolveRun(code, stdout, stderr) 69 | 70 | if (resolved instanceof Error) { 71 | output('runAsync: error') 72 | output(JSON.stringify(resolved, null, 2)) 73 | output('runAsync: reject promise') 74 | reject(resolved) 75 | } else { 76 | output('runAsync: success') 77 | output(JSON.stringify(resolved, null, 2)) 78 | output('runAsync: resolve promise') 79 | resolve(resolved) 80 | } 81 | } 82 | 83 | cp.on('error', onError).on('close', onClose) 84 | }) 85 | 86 | ; (promise as any).cp = cp 87 | 88 | return promise 89 | } 90 | 91 | function resolveRun(exitCode, stdout, stderr) { 92 | stdout = stdout && stdout.toString() 93 | stderr = stderr && stderr.toString() 94 | 95 | if (exitCode !== 0) { 96 | return Object.assign(new Error(`Command failed, exited with code #${exitCode}`), { 97 | exitCode, 98 | stdout, 99 | stderr, 100 | }) 101 | } 102 | 103 | return { 104 | stdout, 105 | stderr, 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/beautifyHtml.ts: -------------------------------------------------------------------------------- 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 | function preAction(php: string): string { 27 | let scriptStyleRanges = getScriptStyleRanges(php) 28 | let strArr = [] 29 | let tokens = new phpParser().tokenGetAll(php) 30 | let c = tokens.length 31 | let index = 0 32 | for (let i = 0; i < c; i++) { 33 | let t = tokens[i] 34 | if (inScriptStyleTag(scriptStyleRanges, index)) { 35 | if (typeof t == 'object') { 36 | if (t[0] == 'T_OPEN_TAG' || t[0] == 'T_OPEN_TAG_WITH_ECHO') { 37 | strArr.push('/*%pcs-comment-start#' + t[1]) 38 | } else if (t[0] == 'T_CLOSE_TAG') { 39 | // fix new line issue 40 | let ms = t[1].match(/(\S+)(\s+)$/) 41 | if (ms) { 42 | strArr.push(ms[1] + '%pcs-comment-end#*/' + ms[2]) 43 | } else { 44 | strArr.push(t[1] + '%pcs-comment-end#*/') 45 | } 46 | } else { 47 | if (t[0] == 'T_INLINE_HTML') { 48 | strArr.push(t[1]) 49 | } else { 50 | let str = t[1].replace(/\*\//g, '*%comment-end#/').replace(/"/g, 'pcs%quote#1').replace(/'/g, 'pcs%quote~2') 51 | strArr.push(str) 52 | } 53 | } 54 | index += t[1].length 55 | } else { 56 | strArr.push(t) 57 | index += t.length 58 | } 59 | } else { 60 | if (typeof t == 'object') { 61 | if (t[0] == 'T_OPEN_TAG' || t[0] == 'T_OPEN_TAG_WITH_ECHO') { 62 | // ' + ms[2]) 69 | } else { 70 | strArr.push(t[1] + '%pcs-comment-end#-->') 71 | } 72 | } else { 73 | if (t[0] == 'T_INLINE_HTML') { 74 | strArr.push(t[1]) 75 | } else { 76 | let str = t[1].replace(/-->/g, '-%comment-end#->').replace(/"/g, 'pcs%quote#1').replace(/'/g, 'pcs%quote~2') 77 | strArr.push(str) 78 | } 79 | } 80 | index += t[1].length 81 | } else { 82 | strArr.push(t) 83 | index += t.length 84 | } 85 | } 86 | } 87 | if (typeof tokens[c - 1] == 'object' && tokens[c - 1][0] != 'T_CLOSE_TAG' && tokens[c - 1][0] != 'T_INLINE_HTML') { 88 | strArr.push('?>%pcs-comment-end#-->') 89 | } 90 | return strArr.join('') 91 | } 92 | 93 | function afterAction(php: string): string { 94 | return php 95 | // .replace(/\?>\s*%pcs-comment-end#-->\s*$/g, '') 96 | .replace(/%pcs-comment-end#-->/g, '') 97 | .replace(/\s*<\/i>\s*') 99 | .replace(/%pcs-comment-end#\*\//g, '') 100 | .replace(/\/\*%pcs-comment-start#/g, '') 101 | .replace(/\*%comment-end#\//g, '*/') 102 | .replace(/pcs%quote#1/g, '"') 103 | .replace(/pcs%quote~2/g, "'") 104 | } 105 | 106 | /** 107 | * get all script/style tag ranges 108 | * @param {php code} php 109 | */ 110 | function getScriptStyleRanges(php) { 111 | let ranges = [] 112 | let start = 0 113 | let parser = new htmlparser.Parser( 114 | { 115 | onopentag: (name) => { 116 | if (name === 'script' || name === 'style') { 117 | start = parser.startIndex 118 | } 119 | }, 120 | onclosetag: (name) => { 121 | if (name === 'script' || name === 'style') { 122 | ranges.push([start, parser.endIndex]) 123 | } 124 | }, 125 | }, 126 | { 127 | decodeEntities: true, 128 | } 129 | ) 130 | parser.write(php) 131 | parser.end() 132 | return ranges 133 | } 134 | 135 | function inScriptStyleTag(ranges: number[], index: number) { 136 | for (let i = 0, c = ranges.length; i < c; i++) { 137 | if (index >= ranges[i][0] && index <= ranges[i][1]) { 138 | return true 139 | } 140 | } 141 | return false 142 | } 143 | 144 | export function beautify(text: string, options: any): string { 145 | //if only php code, return text directly 146 | let indexOfPhp = text.indexOf('') 148 | if (indexOfPhp > -1 && indexOfPhp == text.lastIndexOf('')) { 149 | if (indexOfEndPhp == -1 || indexOfEndPhp + 3 == text.length) { 150 | return text.replace(/^\s+<\?php/i, ' `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 | ```JSON5 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": "@PSR12", 35 | "php-cs-fixer.config": ".php-cs-fixer.php;.php-cs-fixer.dist.php;.php_cs;.php_cs.dist", 36 | "php-cs-fixer.allowRisky": false, 37 | "php-cs-fixer.pathMode": "override", 38 | "php-cs-fixer.ignorePHPVersion": false, 39 | "php-cs-fixer.exclude": [], 40 | "php-cs-fixer.autoFixByBracket": false, 41 | "php-cs-fixer.autoFixBySemicolon": false, 42 | "php-cs-fixer.formatHtml": false, 43 | "php-cs-fixer.documentFormattingProvider": true 44 | } 45 | ``` 46 | 47 | TIPS: If you are using other formatters like Prettier, you can get some conflicts with auto save or other features. To fix it, you have to add this line: 48 | 49 | ```JSON5 50 | // Set PHP CS Fixer as the default formatter for PHP files 51 | // It will avoid conflict with other formatters like prettier etc. 52 | "[php]": { 53 | "editor.defaultFormatter": "junstyle.php-cs-fixer" 54 | }, 55 | ``` 56 | 57 | install php-cs-fixer by composer 58 | 59 | ```JSON 60 | "php-cs-fixer.executablePath": "php-cs-fixer" 61 | ``` 62 | 63 | **TIP:** try "php-cs-fixer.bat" on **Windows**. 64 | 65 | or use phar file 66 | 67 | ```JSON 68 | "php-cs-fixer.executablePath": "/full/path/of/php-cs-fixer.phar" 69 | ``` 70 | 71 | You also have `executablePathWindows` available if you want to specify Windows specific path. Useful if you share your workspace settings among different environments. 72 | 73 | executablePath can use ${workspaceFolder} as workspace first root folder path. 74 | 75 | [executablePath, executablePathWindows, config] can use "~/" as user home directory on os. 76 | 77 | Additionally you can configure this extension to execute on save. 78 | 79 | ```JSON 80 | "php-cs-fixer.onsave": true 81 | ``` 82 | 83 | you can format html at the same time. 84 | 85 | ```JSON 86 | "php-cs-fixer.formatHtml": true 87 | ``` 88 | 89 | You can use a config file from a list of semicolon separated values 90 | 91 | ```JSON 92 | "php-cs-fixer.config": ".php-cs-fixer.php;.php-cs-fixer.dist.php;.php_cs;.php_cs.dist" 93 | ``` 94 | 95 | config file can place in workspace root folder or .vscode folder or any other folders: 96 | 97 | ```JSON 98 | "php-cs-fixer.config": "/full/config/file/path" 99 | ``` 100 | 101 | Relative paths are only considered when a workspace folder is open. 102 | 103 | config file .php-cs-fixer.php example 104 | 105 | ```php 106 | setRules([ 110 | '@PSR12' => true, 111 | 'array_indentation' => true, 112 | 'array_syntax' => ['syntax' => 'short'], 113 | 'combine_consecutive_unsets' => true, 114 | 'class_attributes_separation' => ['elements' => ['method' => 'one',]], 115 | 'multiline_whitespace_before_semicolons' => false, 116 | 'single_quote' => true, 117 | 118 | 'binary_operator_spaces' => [ 119 | 'operators' => [ 120 | // '=>' => 'align', 121 | // '=' => 'align' 122 | ] 123 | ], 124 | // 'blank_line_after_opening_tag' => true, 125 | // 'blank_line_before_statement' => true, 126 | 'braces' => [ 127 | 'allow_single_line_closure' => true, 128 | ], 129 | // 'cast_spaces' => true, 130 | // 'class_definition' => array('singleLine' => true), 131 | 'concat_space' => ['spacing' => 'one'], 132 | 'declare_equal_normalize' => true, 133 | 'function_typehint_space' => true, 134 | 'single_line_comment_style' => ['comment_types' => ['hash']], 135 | 'include' => true, 136 | 'lowercase_cast' => true, 137 | // 'native_function_casing' => true, 138 | // 'new_with_braces' => true, 139 | // 'no_blank_lines_after_class_opening' => true, 140 | // 'no_blank_lines_after_phpdoc' => true, 141 | 'no_blank_lines_before_namespace' => true, 142 | // 'no_empty_comment' => true, 143 | // 'no_empty_phpdoc' => true, 144 | // 'no_empty_statement' => true, 145 | 'no_extra_blank_lines' => [ 146 | 'tokens' => [ 147 | 'curly_brace_block', 148 | 'extra', 149 | // 'parenthesis_brace_block', 150 | // 'square_brace_block', 151 | 'throw', 152 | 'use', 153 | ] 154 | ], 155 | // 'no_leading_import_slash' => true, 156 | // 'no_leading_namespace_whitespace' => true, 157 | // 'no_mixed_echo_print' => array('use' => 'echo'), 158 | 'no_multiline_whitespace_around_double_arrow' => true, 159 | // 'no_short_bool_cast' => true, 160 | // 'no_singleline_whitespace_before_semicolons' => true, 161 | 'no_spaces_around_offset' => true, 162 | // 'no_trailing_comma_in_list_call' => true, 163 | // 'no_trailing_comma_in_singleline_array' => true, 164 | // 'no_unneeded_control_parentheses' => true, 165 | // 'no_unused_imports' => true, 166 | 'no_whitespace_before_comma_in_array' => true, 167 | 'no_whitespace_in_blank_line' => true, 168 | // 'normalize_index_brace' => true, 169 | 'object_operator_without_whitespace' => true, 170 | // 'php_unit_fqcn_annotation' => true, 171 | // 'phpdoc_align' => true, 172 | // 'phpdoc_annotation_without_dot' => true, 173 | // 'phpdoc_indent' => true, 174 | // 'phpdoc_inline_tag' => true, 175 | // 'phpdoc_no_access' => true, 176 | // 'phpdoc_no_alias_tag' => true, 177 | // 'phpdoc_no_empty_return' => true, 178 | // 'phpdoc_no_package' => true, 179 | // 'phpdoc_no_useless_inheritdoc' => true, 180 | // 'phpdoc_return_self_reference' => true, 181 | // 'phpdoc_scalar' => true, 182 | // 'phpdoc_separation' => true, 183 | // 'phpdoc_single_line_var_spacing' => true, 184 | // 'phpdoc_summary' => true, 185 | // 'phpdoc_to_comment' => true, 186 | // 'phpdoc_trim' => true, 187 | // 'phpdoc_types' => true, 188 | // 'phpdoc_var_without_name' => true, 189 | // 'increment_style' => true, 190 | // 'return_type_declaration' => true, 191 | // 'self_accessor' => true, 192 | // 'short_scalar_cast' => true, 193 | // 'single_blank_line_before_namespace' => true, 194 | // 'single_class_element_per_statement' => true, 195 | // 'space_after_semicolon' => true, 196 | // 'standardize_not_equals' => true, 197 | 'ternary_operator_spaces' => true, 198 | // 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 199 | 'trim_array_spaces' => true, 200 | 'unary_operator_spaces' => true, 201 | 'whitespace_after_comma_in_array' => true, 202 | 'space_after_semicolon' => true, 203 | // 'single_blank_line_at_eof' => false 204 | ]) 205 | // ->setIndent("\t") 206 | ->setLineEnding("\n") 207 | ; 208 | ``` 209 | 210 | If you have installed other PHP related extensions to VS Code it may happen that 211 | another formatter is used per default. You can force this extension to be used 212 | per default by adding this to your settings: 213 | 214 | ```JSON 215 | "[php]": { 216 | "editor.defaultFormatter": "junstyle.php-cs-fixer" 217 | } 218 | ``` 219 | 220 | ## Auto fix 221 | 222 | ```text 223 | 1. by Bracket, when press down the key } auto fix the code in the brackets {} 224 | 2. by Semicolon, when press down the key ; auto fix the code at the current line 225 | ``` 226 | 227 | For more information please visit: [https://github.com/FriendsOfPHP/PHP-CS-Fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer) 228 | 229 | ## License 230 | 231 | MIT 232 | 233 | ## If you love this extension 234 | 235 | [Buy Me A Coffee](https://www.buymeacoffee.com/junstyle) 236 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import anymatch from 'anymatch' 3 | import { SpawnOptionsWithoutStdio } from 'child_process' 4 | import * as fs from 'fs' 5 | import * as os from 'os' 6 | import * as path from 'path' 7 | import { commands, ExtensionContext, FormattingOptions, languages, Position, Range, TextDocument, TextDocumentChangeEvent, TextEdit, Uri, window, workspace, WorkspaceFolder } from 'vscode' 8 | import { beautify } from './beautifyHtml' 9 | import { PHPCSFixerConfig } from './index.d' 10 | import { clearOutput, disposeOutput, hideStatusBar, output, showOutput, statusInfo } from './output' 11 | import { runAsync } from './runAsync' 12 | const TmpDir = os.tmpdir() 13 | const HomeDir = os.homedir() 14 | let isRunning = false 15 | let lastActiveEditor = null 16 | 17 | class PHPCSFixer extends PHPCSFixerConfig { 18 | constructor() { 19 | super() 20 | this.loadSettings() 21 | this.checkUpdate() 22 | } 23 | 24 | loadSettings() { 25 | const config = workspace.getConfiguration('php-cs-fixer') 26 | this.onsave = config.get('onsave', false) 27 | this.autoFixByBracket = config.get('autoFixByBracket', true) 28 | this.autoFixBySemicolon = config.get('autoFixBySemicolon', false) 29 | this.executablePath = config.get('executablePath', process.platform === 'win32' ? 'php-cs-fixer.bat' : 'php-cs-fixer') 30 | if (process.platform == 'win32' && config.get('executablePathWindows', '').length > 0) { 31 | this.executablePath = config.get('executablePathWindows') 32 | } 33 | this.executablePath = this.resolveVscodeExpressions(this.executablePath) 34 | this.rules = config.get('rules', '@PSR12') 35 | if (typeof this.rules == 'object') { 36 | this.rules = JSON.stringify(this.rules) 37 | } 38 | this.config = config.get('config', '.php-cs-fixer.php;.php-cs-fixer.dist.php;.php_cs;.php_cs.dist') 39 | this.formatHtml = config.get('formatHtml', false) 40 | this.documentFormattingProvider = config.get('documentFormattingProvider', true) 41 | this.allowRisky = config.get('allowRisky', false) 42 | this.pathMode = config.get('pathMode', 'override') 43 | this.ignorePHPVersion = config.get('ignorePHPVersion', false) 44 | this.exclude = config.get('exclude', []) 45 | this.tmpDir = config.get('tmpDir', '') 46 | 47 | if (this.executablePath.endsWith('.phar')) { 48 | this.pharPath = this.executablePath.replace(/^php[^ ]* /i, '') 49 | this.executablePath = workspace.getConfiguration('php').get('validate.executablePath', 'php') 50 | if (!this.executablePath) { 51 | this.executablePath = 'php' 52 | } 53 | } else { 54 | this.pharPath = null 55 | } 56 | 57 | this.editorFormatOnSave = workspace.getConfiguration('editor').get('formatOnSave') 58 | // this.fileAutoSave = workspace.getConfiguration('files').get('autoSave') 59 | // this.fileAutoSaveDelay = workspace.getConfiguration('files').get('autoSaveDelay', 1000) 60 | } 61 | 62 | /** 63 | * Gets the workspace folder containing the given uri or `null` if no 64 | * workspace folder contains it and it cannot be reasonably inferred. 65 | */ 66 | getActiveWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { 67 | let candidate = workspace.getWorkspaceFolder(uri) 68 | 69 | // Fallback to using the single root workspace's folder. Multi-root 70 | // workspaces should not be used because its impossible to guess which one 71 | // the developer intended to use. 72 | if (candidate === undefined && workspace.workspaceFolders?.length === 1) { 73 | candidate = workspace.workspaceFolders[0] 74 | } 75 | 76 | return candidate 77 | } 78 | 79 | /** 80 | * Resolves and interpolates vscode expressions in a given string. 81 | * 82 | * Supports the following expressions: 83 | * - "${workspaceFolder}" or "${workspaceRoot}" (deprecated). Resolves to the 84 | * workspace folder that contains the given `context.uri`. 85 | * - "${extensionPath}" Resolves to the root folder of this extension. 86 | * - "~" Resolves to the user's home directory. 87 | * 88 | * @param context Any additional context that may be necessary to resolve 89 | * expressions. Expressions with missing context are left as is. 90 | */ 91 | resolveVscodeExpressions(input: string, context: { uri?: Uri } = {}) { 92 | const pattern = /^\$\{workspace(Root|Folder)\}/ 93 | if (pattern.test(input) && context.uri) { 94 | const workspaceFolder = this.getActiveWorkspaceFolder(context.uri) 95 | // As of time of writing only workspace folders on disk are supported 96 | // since the php-cs-fixer binary expects to work off local files. UNC 97 | // filepaths may be supported but this is untested. 98 | if (workspaceFolder != null && workspaceFolder.uri.scheme === 'file') { 99 | input = input.replace(pattern, workspaceFolder.uri.fsPath) 100 | } 101 | } 102 | 103 | input = input.replace('${extensionPath}', __dirname) 104 | input = input.replace(/^~\//, os.homedir() + '/') 105 | 106 | return path.normalize(input) 107 | } 108 | 109 | getRealExecutablePath(uri: Uri): string | undefined { 110 | return this.resolveVscodeExpressions(this.executablePath, { uri }) 111 | } 112 | 113 | getArgs(uri: Uri, filePath: string = null): string[] { 114 | filePath = filePath || uri.fsPath 115 | 116 | let args = ['fix', '--using-cache=no', '--format=json'] 117 | if (this.pharPath != null) { 118 | args.unshift(this.resolveVscodeExpressions(this.pharPath, { uri })) 119 | } 120 | let useConfig = false 121 | if (this.config.length > 0) { 122 | let rootUri = this.getActiveWorkspaceFolder(uri)?.uri 123 | let configFiles = this.config 124 | .split(';') // allow multiple files definitions semicolon separated values 125 | .filter((file) => '' !== file) // do not include empty definitions 126 | .map((file) => file.replace(/^~\//, os.homedir() + '/')) // replace ~/ with home dir 127 | 128 | // include also {workspace.rootUri}/.vscode/ & {workspace.rootUri}/ 129 | let searchUris = [] 130 | if (rootUri != null && rootUri.scheme === 'file') { 131 | searchUris = [Uri.joinPath(rootUri, '.vscode'), rootUri] 132 | } 133 | 134 | const files = [] 135 | for (const file of configFiles) { 136 | if (path.isAbsolute(file)) { 137 | files.push(file) 138 | } else { 139 | for (const searchUri of searchUris) { 140 | files.push(Uri.joinPath(searchUri, file).fsPath) 141 | } 142 | } 143 | } 144 | 145 | for (let i = 0, len = files.length; i < len; i++) { 146 | let c = files[i] 147 | if (fs.existsSync(c)) { 148 | if (process.platform == 'win32') { 149 | args.push('--config="' + c.replace(/"/g, "\\\"") + '"') 150 | } else { 151 | args.push('--config=' + c) 152 | } 153 | useConfig = true 154 | break 155 | } 156 | } 157 | } 158 | if (!useConfig && this.rules) { 159 | if (process.platform == 'win32') { 160 | args.push('--rules="' + (this.rules as string).replace(/"/g, "\\\"") + '"') 161 | } else { 162 | args.push('--rules=' + this.rules) 163 | } 164 | } 165 | if (this.allowRisky) { 166 | args.push('--allow-risky=yes') 167 | } 168 | 169 | if (filePath.startsWith(TmpDir)) { 170 | args.push('--path-mode=override') 171 | } else { 172 | args.push('--path-mode=' + this.pathMode) 173 | } 174 | args.push(filePath) 175 | 176 | return args 177 | } 178 | 179 | format(text: string | Buffer, uri: Uri, isDiff: boolean = false, isPartial: boolean = false): Promise { 180 | isRunning = true 181 | clearOutput() 182 | isPartial || statusInfo('formatting') 183 | 184 | let filePath: string 185 | // if interval between two operations too short, see: https://github.com/junstyle/vscode-php-cs-fixer/issues/76 186 | // so set different filePath for partial codes; 187 | if (isPartial) { 188 | filePath = TmpDir + '/php-cs-fixer-partial.php' 189 | } else { 190 | let tmpDirs = [this.tmpDir, TmpDir, HomeDir].filter(Boolean); 191 | for (let i = 0; i < tmpDirs.length; i++) { 192 | filePath = path.join(tmpDirs[i], 'pcf-tmp' + Math.random(), uri.fsPath.replace(/^.*[\\/]/, '')) 193 | try { 194 | fs.mkdirSync(path.dirname(filePath), { recursive: true }) 195 | this.tmpDir = tmpDirs[i] 196 | break; 197 | } catch (err) { 198 | console.error(err) 199 | filePath = '' 200 | } 201 | } 202 | if (!filePath) { 203 | statusInfo("can't make tmp dir, please check the php-cs-fixer settings, set a writable dir to tmpDir.") 204 | return Promise.reject() 205 | } 206 | } 207 | 208 | fs.writeFileSync(filePath, text) 209 | 210 | const args = this.getArgs(uri, filePath) 211 | const opts: SpawnOptionsWithoutStdio = {} 212 | if (uri.scheme == 'file') { 213 | opts.cwd = path.dirname(uri.fsPath) 214 | } 215 | if (this.ignorePHPVersion) { 216 | opts.env = Object.create(process.env) 217 | opts.env.PHP_CS_FIXER_IGNORE_ENV = "1" 218 | } 219 | 220 | return new Promise((resolve, reject) => { 221 | runAsync(this.getRealExecutablePath(uri), args, opts) 222 | .then(({ stdout, stderr }) => { 223 | output(stdout) 224 | 225 | if (isDiff) { 226 | resolve(filePath) 227 | } else { 228 | let result = JSON.parse(stdout) 229 | if (result && result.files.length > 0) { 230 | resolve(fs.readFileSync(filePath, 'utf-8')) 231 | } else { 232 | let lines = stderr.split(/\r?\n/).filter(Boolean) 233 | if (lines.length > 1) { 234 | output(stderr) 235 | isPartial || statusInfo(lines[1]) 236 | return reject(new Error(stderr)) 237 | } else { 238 | resolve(text.toString()) 239 | } 240 | } 241 | } 242 | hideStatusBar() 243 | }) 244 | .catch((err) => { 245 | reject(err) 246 | output(err.stderr || JSON.stringify(err, null, 2)) 247 | isPartial || statusInfo('failed') 248 | 249 | if (err.code == 'ENOENT') { 250 | this.errorTip() 251 | } else if (err.exitCode) { 252 | const msgs = { 253 | 1: err.stdout || 'General error (or PHP minimal requirement not matched).', 254 | 16: 'Configuration error of the application.', // The path "/file/path.php" is not readable 255 | 32: 'Configuration error of a Fixer.', 256 | 64: 'Exception raised within the application.', 257 | 255: err.stderr?.match(/PHP (?:Fatal|Parse) error:\s*Uncaught Error:[^\r?\n]+/)?.[0] || 'PHP Fatal error, click to show output.', 258 | } 259 | isPartial || statusInfo(msgs[err.exitCode]) 260 | } 261 | }) 262 | .finally(() => { 263 | isRunning = false 264 | if (!isDiff && !isPartial) { 265 | fs.rm(path.dirname(filePath), { recursive: true, force: true }, function (err) { err && console.error(err) }) 266 | } 267 | }) 268 | }) 269 | } 270 | 271 | fix(uri: Uri) { 272 | isRunning = true 273 | clearOutput() 274 | statusInfo('fixing') 275 | 276 | const args = this.getArgs(uri) 277 | const opts: SpawnOptionsWithoutStdio = {} 278 | if (uri.fsPath != '') { 279 | opts.cwd = path.dirname(uri.fsPath) 280 | } 281 | if (this.ignorePHPVersion) { 282 | opts.env = Object.create(process.env) 283 | opts.env.PHP_CS_FIXER_IGNORE_ENV = "1" 284 | } 285 | 286 | runAsync(this.getRealExecutablePath(uri), args, opts, (data) => { 287 | output(data.toString()) 288 | }) 289 | .then(({ stdout }) => { 290 | hideStatusBar() 291 | }) 292 | .catch((err) => { 293 | statusInfo('failed') 294 | if (err.code == 'ENOENT') { 295 | this.errorTip() 296 | } 297 | }) 298 | .finally(() => { 299 | isRunning = false 300 | }) 301 | } 302 | 303 | diff(uri: Uri) { 304 | this.format(fs.readFileSync(uri.fsPath), uri, true) 305 | .then((tempFilePath) => { 306 | commands.executeCommand('vscode.diff', uri, Uri.file(tempFilePath), 'diff') 307 | }) 308 | .catch((err) => { 309 | console.error(err) 310 | }) 311 | } 312 | 313 | doAutoFixByBracket(event: TextDocumentChangeEvent) { 314 | if (event.contentChanges.length == 0) return 315 | let pressedKey = event.contentChanges[0].text 316 | // console.log(pressedKey); 317 | if (!/^\s*\}$/.test(pressedKey)) { 318 | return 319 | } 320 | 321 | let editor = window.activeTextEditor 322 | let document = editor.document 323 | let originalStart = editor.selection.start 324 | commands.executeCommand('editor.action.jumpToBracket').then(() => { 325 | let start = editor.selection.start 326 | let offsetStart0 = document.offsetAt(originalStart) 327 | let offsetStart1 = document.offsetAt(start) 328 | if (offsetStart0 == offsetStart1) { 329 | return 330 | } 331 | 332 | let nextChar = document.getText(new Range(start, start.translate(0, 1))) 333 | if (offsetStart0 - offsetStart1 < 3 || nextChar != '{') { 334 | // jumpToBracket to wrong match bracket, do nothing 335 | commands.executeCommand('cursorUndo') 336 | return 337 | } 338 | 339 | let line = document.lineAt(start) 340 | let code = ' fixed.replace(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\r?\n/, '').replace(/\s*$/, '') 342 | let searchIndex = -1 343 | if (/^\s*\{\s*$/.test(line.text)) { 344 | // check previous line 345 | let preline = document.lineAt(line.lineNumber - 1) 346 | 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) 347 | if (searchIndex > -1) { 348 | line = preline 349 | } 350 | } else { 351 | 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) 352 | } 353 | 354 | if (searchIndex > -1) { 355 | start = line.range.start 356 | } else { 357 | // indent + if(1) 358 | code += line.text.match(/^(\s*)\S+/)[1] + 'if(1)' 359 | dealFun = (fixed) => { 360 | let match = fixed.match(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\s+?if\s*\(\s*1\s*\)\s*(\{[\s\S]+?\})\s*$/i) 361 | if (match != null) { 362 | fixed = match[1] 363 | } else { 364 | fixed = '' 365 | } 366 | return fixed 367 | } 368 | } 369 | 370 | commands.executeCommand('cursorUndo').then(() => { 371 | let end = editor.selection.start 372 | let range = new Range(start, end) 373 | let originalText = code + document.getText(range) 374 | 375 | this.format(originalText, document.uri, false, true) 376 | .then((text) => { 377 | text = dealFun(text) 378 | if (text != dealFun(originalText)) { 379 | editor 380 | .edit((builder) => { 381 | builder.replace(range, text) 382 | }) 383 | .then(() => { 384 | if (editor.selections.length > 0) { 385 | commands.executeCommand('cancelSelection') 386 | } 387 | }) 388 | } 389 | }) 390 | .catch((err) => { 391 | console.log(err) 392 | }) 393 | }) 394 | }) 395 | } 396 | 397 | doAutoFixBySemicolon(event: TextDocumentChangeEvent) { 398 | if (event.contentChanges.length == 0) return 399 | let pressedKey = event.contentChanges[0].text 400 | // console.log(pressedKey); 401 | if (pressedKey != ';') { 402 | return 403 | } 404 | let editor = window.activeTextEditor 405 | let line = editor.document.lineAt(editor.selection.start) 406 | if (line.text.length < 5) { 407 | return 408 | } 409 | // only at last char 410 | if (line.range.end.character != editor.selection.end.character + 1) { 411 | return 412 | } 413 | 414 | let indent = line.text.match(/^(\s*)/)[1] 415 | let dealFun = (fixed) => fixed.replace(/^<\?php[\s\S]+?\$__pcf__spliter\s*=\s*0;\r?\n/, '').replace(/\s+$/, '') 416 | let range = line.range 417 | let originalText = ' { 421 | text = dealFun(text) 422 | if (text != dealFun(originalText)) { 423 | text = indent + text 424 | editor 425 | .edit((builder) => { 426 | builder.replace(range, text) 427 | }) 428 | .then(() => { 429 | if (editor.selections.length > 0) { 430 | commands.executeCommand('cancelSelection') 431 | } 432 | }) 433 | } 434 | }) 435 | .catch((err) => { 436 | console.log(err) 437 | }) 438 | } 439 | 440 | formattingProvider(document: TextDocument, options: FormattingOptions): Promise { 441 | if (this.isExcluded(document)) { 442 | return 443 | } 444 | 445 | // only activeTextEditor, or last activeTextEditor 446 | // if (window.activeTextEditor == undefined 447 | // || (window.activeTextEditor.document.uri.toString() != document.uri.toString() && lastActiveEditor != document.uri.toString())) 448 | // return 449 | 450 | isRunning = false 451 | return new Promise((resolve, reject) => { 452 | let originalText = document.getText() 453 | let lastLine = document.lineAt(document.lineCount - 1) 454 | let range = new Range(new Position(0, 0), lastLine.range.end) 455 | let htmlOptions = Object.assign(options, workspace.getConfiguration('html').get('format')) 456 | let originalText2 = this.formatHtml ? beautify(originalText, htmlOptions) : originalText 457 | 458 | this.format(originalText2, document.uri) 459 | .then((text) => { 460 | if (text && text != originalText) { 461 | resolve([new TextEdit(range, text)]) 462 | } else { 463 | resolve([]) 464 | } 465 | }) 466 | .catch((err) => { 467 | console.log(err) 468 | reject() 469 | }) 470 | }) 471 | } 472 | 473 | rangeFormattingProvider(document: TextDocument, range: Range): Promise { 474 | if (this.isExcluded(document)) { 475 | return 476 | } 477 | 478 | // only activeTextEditor, or last activeTextEditor 479 | // if (window.activeTextEditor == undefined 480 | // || (window.activeTextEditor.document.uri.toString() != document.uri.toString() && lastActiveEditor != document.uri.toString())) 481 | // return 482 | 483 | isRunning = false 484 | return new Promise((resolve, reject) => { 485 | let originalText = document.getText(range) 486 | if (originalText.replace(/\s+/g, '').length == 0) { 487 | reject() 488 | return 489 | } 490 | let addPHPTag = false 491 | if (originalText.search(/^\s*<\?php/i) == -1) { 492 | originalText = ' { 498 | if (addPHPTag) { 499 | text = text.replace(/^<\?php\r?\n/, '') 500 | } 501 | if (text && text != originalText) { 502 | resolve([new TextEdit(range, text)]) 503 | } else { 504 | resolve([]) 505 | } 506 | }) 507 | .catch((err) => { 508 | console.log(err) 509 | reject() 510 | }) 511 | }) 512 | } 513 | 514 | isExcluded(document: TextDocument): boolean { 515 | if (this.exclude.length > 0 && document.uri.scheme == 'file' && !document.isUntitled) { 516 | return anymatch(this.exclude, document.uri.path) 517 | } 518 | return false 519 | } 520 | 521 | errorTip() { 522 | window.showErrorMessage('PHP CS Fixer: executablePath not found. Try setting `"php-cs-fixer.executablePath": "${extensionPath}/php-cs-fixer.phar"` and try again.', 'Open Output').then((t) => { 523 | if (t == 'Open Output') { 524 | showOutput() 525 | } 526 | }) 527 | // const config = workspace.getConfiguration('php-cs-fixer') 528 | // config.update('executablePath', '${extensionPath}/php-cs-fixer.phar', true) 529 | } 530 | 531 | checkUpdate() { 532 | setTimeout(() => { 533 | let config = workspace.getConfiguration('php-cs-fixer') 534 | let executablePath = config.get('executablePath', 'php-cs-fixer') 535 | let lastDownload = config.get('lastDownload', 1) 536 | if (lastDownload !== 0 && executablePath == '${extensionPath}/php-cs-fixer.phar' && lastDownload + 1000 * 3600 * 24 * 7 < new Date().getTime()) { 537 | console.log('php-cs-fixer: check for updating...') 538 | const { DownloaderHelper } = require('node-downloader-helper') 539 | let dl = new DownloaderHelper('https://cs.symfony.com/download/php-cs-fixer-v3.phar', __dirname, { fileName: 'php-cs-fixer.phar.tmp', override: true }) 540 | dl.on('end', () => { 541 | fs.unlinkSync(path.join(__dirname, 'php-cs-fixer.phar')) 542 | fs.renameSync(path.join(__dirname, 'php-cs-fixer.phar.tmp'), path.join(__dirname, 'php-cs-fixer.phar')) 543 | config.update('lastDownload', new Date().getTime(), true) 544 | }) 545 | dl.start() 546 | } 547 | }, 1000 * 60) 548 | } 549 | } 550 | 551 | exports.activate = (context: ExtensionContext) => { 552 | const pcf = new PHPCSFixer() 553 | 554 | // context.subscriptions.push(window.onDidChangeActiveTextEditor(te => { 555 | // if (pcf.fileAutoSave != 'off') { 556 | // setTimeout(() => lastActiveEditor = te == undefined ? undefined : te.document.uri.toString(), pcf.fileAutoSaveDelay + 100) 557 | // } 558 | // })) 559 | 560 | context.subscriptions.push( 561 | workspace.onWillSaveTextDocument((event) => { 562 | if (event.document.languageId == 'php' && pcf.onsave && pcf.editorFormatOnSave == false) { 563 | event.waitUntil(pcf.formattingProvider(event.document, {} as any)) 564 | } 565 | }) 566 | ) 567 | 568 | context.subscriptions.push( 569 | commands.registerTextEditorCommand('php-cs-fixer.fix', (textEditor) => { 570 | if (textEditor.document.languageId == 'php') { 571 | pcf.formattingProvider(textEditor.document, {} as any).then((tes) => { 572 | if (tes && tes.length > 0) { 573 | textEditor.edit((eb) => { 574 | eb.replace(tes[0].range, tes[0].newText) 575 | }) 576 | } 577 | }) 578 | } 579 | }) 580 | ) 581 | 582 | context.subscriptions.push( 583 | workspace.onDidChangeTextDocument((event) => { 584 | if (event.document.languageId == 'php' && isRunning == false) { 585 | if (pcf.isExcluded(event.document)) { 586 | return 587 | } 588 | 589 | if (pcf.autoFixByBracket) { 590 | pcf.doAutoFixByBracket(event) 591 | } 592 | if (pcf.autoFixBySemicolon) { 593 | pcf.doAutoFixBySemicolon(event) 594 | } 595 | } 596 | }) 597 | ) 598 | 599 | context.subscriptions.push( 600 | workspace.onDidChangeConfiguration(() => { 601 | pcf.loadSettings() 602 | }) 603 | ) 604 | 605 | if (pcf.documentFormattingProvider) { 606 | context.subscriptions.push( 607 | languages.registerDocumentFormattingEditProvider('php', { 608 | provideDocumentFormattingEdits: (document, options, token) => { 609 | return pcf.formattingProvider(document, options) 610 | }, 611 | }) 612 | ) 613 | 614 | context.subscriptions.push( 615 | languages.registerDocumentRangeFormattingEditProvider('php', { 616 | provideDocumentRangeFormattingEdits: (document, range, options, token) => { 617 | return pcf.rangeFormattingProvider(document, range) 618 | }, 619 | }) 620 | ) 621 | } 622 | 623 | context.subscriptions.push( 624 | commands.registerCommand('php-cs-fixer.fix2', (f) => { 625 | if (f == undefined) { 626 | let editor = window.activeTextEditor 627 | if (editor != undefined && editor.document.languageId == 'php') { 628 | f = editor.document.uri 629 | } 630 | } 631 | if (f && f.scheme == 'file') { 632 | let stat = fs.statSync(f.fsPath) 633 | if (stat.isDirectory()) { 634 | showOutput() 635 | } 636 | if (f != undefined) { 637 | pcf.fix(f) 638 | } 639 | } 640 | }) 641 | ) 642 | 643 | context.subscriptions.push( 644 | commands.registerCommand('php-cs-fixer.diff', (f) => { 645 | if (f == undefined) { 646 | let editor = window.activeTextEditor 647 | if (editor != undefined && editor.document.languageId == 'php') { 648 | f = editor.document.uri 649 | } 650 | } 651 | if (f && f.scheme == 'file') { 652 | if (f != undefined) { 653 | pcf.diff(f) 654 | } 655 | } 656 | }) 657 | ) 658 | 659 | context.subscriptions.push(commands.registerCommand('php-cs-fixer.showOutput', showOutput)) 660 | } 661 | 662 | exports.deactivate = () => { 663 | disposeOutput() 664 | } 665 | --------------------------------------------------------------------------------