├── 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 |
--------------------------------------------------------------------------------