├── .gitignore ├── .no-sublime-package ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Main.sublime-menu ├── PHP_CodeSniffer.py ├── PHP_CodeSniffer.sublime-settings ├── README.md ├── STPluginReport.php └── icons ├── error.png └── warning.png /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.pyc 3 | -------------------------------------------------------------------------------- /.no-sublime-package: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/squizlabs/sublime-PHP_CodeSniffer/e7fa40d3a7ad27553e8caf0014567cace82ed785/.no-sublime-package -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+s"], 4 | "command": "phpcs" 5 | }, 6 | { 7 | "keys": ["alt+shift+s"], 8 | "command": "phpcbf" 9 | } 10 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+s"], 4 | "command": "phpcs" 5 | }, 6 | { 7 | "keys": ["alt+shift+s"], 8 | "command": "phpcbf" 9 | } 10 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+s"], 4 | "command": "phpcs" 5 | }, 6 | { 7 | "keys": ["alt+shift+s"], 8 | "command": "phpcbf" 9 | } 10 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "PHP_CodeSniffer: Check File", 4 | "command": "phpcs" 5 | }, 6 | { 7 | "caption": "PHP_CodeSniffer: Fix File", 8 | "command": "phpcbf" 9 | }, 10 | { 11 | "caption": "Preferences: PHP_CodeSniffer Settings – Default", 12 | "command": "open_file", "args": 13 | { 14 | "file": "${packages}/PHP_CodeSniffer/PHP_CodeSniffer.sublime-settings" 15 | } 16 | }, 17 | { 18 | "caption": "Preferences: PHP_CodeSniffer Settings – User", 19 | "command": "open_file", "args": 20 | { 21 | "file": "${packages}/User/PHP_CodeSniffer.sublime-settings" 22 | } 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "tools", 4 | "children": 5 | [ 6 | { 7 | "caption": "PHP_CodeSniffer", 8 | "id": "phpcs", 9 | "children": 10 | [ 11 | { 12 | "caption": "Check File", 13 | "command": "phpcs" 14 | }, 15 | { 16 | "caption": "Fix File", 17 | "command": "phpcbf" 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | { 24 | "id": "preferences", 25 | "children": 26 | [ 27 | { 28 | "caption": "Package Settings", 29 | "id": "package-settings", 30 | "children": 31 | [ 32 | { 33 | "caption": "PHP_CodeSniffer", 34 | "children": 35 | [ 36 | { 37 | "caption": "Settings – Default", 38 | "command": "open_file", 39 | "args": 40 | { 41 | "file": "${packages}/PHP_CodeSniffer/PHP_CodeSniffer.sublime-settings" 42 | } 43 | }, 44 | { 45 | "caption": "Settings – User", 46 | "command": "open_file", 47 | "args": 48 | { 49 | "file": "${packages}/User/PHP_CodeSniffer.sublime-settings" 50 | } 51 | } 52 | ] 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | -------------------------------------------------------------------------------- /PHP_CodeSniffer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sublime 4 | import sublime_plugin 5 | import subprocess 6 | import string 7 | import difflib 8 | import threading 9 | 10 | SETTINGS_FILE = 'PHP_CodeSniffer.sublime-settings' 11 | RESULT_VIEW_NAME = 'phpcs_result_view' 12 | 13 | settings = sublime.load_settings(SETTINGS_FILE) 14 | 15 | class PHP_CodeSniffer: 16 | # Type of the view, phpcs or phpcbf. 17 | file_view = None 18 | view_type = None 19 | window = None 20 | processed = False 21 | output_view = None 22 | process_anim_idx = 0 23 | process_anim = { 24 | 'windows': ['|', '/', '-', '\\'], 25 | 'linux': ['|', '/', '-', '\\'], 26 | 'osx': [u'\u25d0', u'\u25d3', u'\u25d1', u'\u25d2'] 27 | } 28 | regions = [] 29 | 30 | def run(self, window, cmd, msg): 31 | self.window = window 32 | content = window.active_view().substr(sublime.Region(0, window.active_view().size())) 33 | 34 | tm = threading.Thread(target=self.loading_msg, args=([msg])) 35 | tm.start() 36 | 37 | t = threading.Thread(target=self.run_command, args=(self.get_command_args(cmd), cmd, content, window, window.active_view().file_name())) 38 | t.start() 39 | 40 | 41 | def loading_msg(self, msg): 42 | sublime.set_timeout(lambda: self.show_loading_msg(msg), 0) 43 | 44 | 45 | def process_phpcbf_results(self, fixed_content, window, content): 46 | # Remove the gutter markers. 47 | self.window = window 48 | self.file_view = window.active_view() 49 | self.view_type = 'phpcbf' 50 | 51 | # Get the diff between content and the fixed content. 52 | difftxt = self.run_diff(window, content, fixed_content) 53 | self.processed = True 54 | 55 | if not difftxt: 56 | self.clear_view() 57 | return 58 | 59 | self.file_view.erase_regions('errors') 60 | self.file_view.erase_regions('warnings') 61 | 62 | # Store the current viewport position. 63 | scrollPos = self.file_view.viewport_position() 64 | 65 | # Show diff text in the results panel. 66 | self.show_results_view(window, difftxt) 67 | self.set_status_msg(''); 68 | 69 | self.file_view.run_command('set_view_content', {'data':fixed_content, 'replace':True}) 70 | 71 | # After the active view contents are changed set the scroll position back to previous position. 72 | self.file_view.set_viewport_position(scrollPos, False) 73 | 74 | 75 | def run_diff(self, window, origContent, fixed_content): 76 | try: 77 | a = origContent.splitlines() 78 | b = fixed_content.splitlines() 79 | except UnicodeDecodeError as e: 80 | sublime.status_message("Diff only works with UTF-8 files") 81 | return 82 | 83 | # Get the diff between original content and the fixed content. 84 | diff = difflib.unified_diff(a, b, 'Original', 'Fixed', lineterm='') 85 | difftxt = u"\n".join(line for line in diff) 86 | 87 | if difftxt == "": 88 | sublime.status_message('PHP_CodeSniffer did not make any changes') 89 | return 90 | 91 | difftxt = "\n PHP_CodeSniffer made the following fixes to this file:\n\n" + difftxt 92 | return difftxt 93 | 94 | 95 | def process_phpcs_results(self, data, window): 96 | self.processed = True 97 | self.window = window 98 | self.file_view = window.active_view() 99 | self.view_type = 'phpcs' 100 | 101 | if data == '': 102 | self.file_view.erase_regions('errors') 103 | self.file_view.erase_regions('warnings') 104 | window.run_command("hide_panel", {"panel": "output." + RESULT_VIEW_NAME}) 105 | self.set_status_msg('No errors or warnings detected.') 106 | return 107 | 108 | self.show_results_view(window, data) 109 | self.set_status_msg(''); 110 | 111 | # Add gutter markers for each error. 112 | lines = data.decode('utf-8').split("\n") 113 | err_regions = [] 114 | warn_regions = [] 115 | col_regions = [] 116 | msg_type = '' 117 | 118 | for line in lines: 119 | if line.find('Errors:') != -1: 120 | msg_type = 'error' 121 | elif line.find('Warnings:') != -1: 122 | msg_type = 'warning' 123 | else: 124 | match = re.match(r'[^:0-9]+([0-9]+)\s*:', line) 125 | if match: 126 | pt = window.active_view().text_point(int(match.group(1)) - 1, 0) 127 | r = window.active_view().line(pt) 128 | self.regions.append( 129 | { 130 | 'region': r, 131 | 'type': msg_type, 132 | 'message': line 133 | } 134 | ); 135 | 136 | if msg_type == 'error': 137 | err_regions.append(r) 138 | else: 139 | warn_regions.append(r) 140 | 141 | window.active_view().erase_regions('errors') 142 | window.active_view().erase_regions('warnings') 143 | window.active_view().add_regions('errors', err_regions, settings.get('error_scope'), 'Packages/PHP_CodeSniffer/icons/error.png', sublime.DRAW_STIPPLED_UNDERLINE | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE) 144 | window.active_view().add_regions('warnings', warn_regions, settings.get('warning_scope'), 'Packages/PHP_CodeSniffer/icons/warning.png', sublime.DRAW_STIPPLED_UNDERLINE | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE) 145 | 146 | def get_command_args(self, cmd_type): 147 | args = [] 148 | 149 | if settings.get('php_path'): 150 | args.append(settings.get('php_path')) 151 | elif os.name == 'nt': 152 | args.append('php') 153 | 154 | if cmd_type == 'phpcs': 155 | args.append(settings.get('phpcs_path', 'phpcs')) 156 | args.append('--report=' + sublime.packages_path() + '/PHP_CodeSniffer/STPluginReport.php') 157 | else: 158 | args.append(settings.get('phpcbf_path', 'phpcbf')) 159 | 160 | standard_setting = settings.get('phpcs_standard') 161 | standard = '' 162 | 163 | if type(standard_setting) is dict: 164 | for folder in self.window.folders(): 165 | folder_name = os.path.basename(folder) 166 | if folder_name in standard_setting: 167 | standard = standard_setting[folder_name] 168 | break 169 | 170 | if standard == '' and '_default' in standard_setting: 171 | standard = standard_setting['_default'] 172 | else: 173 | standard = standard_setting 174 | 175 | if settings.get('phpcs_standard'): 176 | args.append('--standard=' + standard) 177 | 178 | args.append('-') 179 | 180 | if settings.get('additional_args'): 181 | args += settings.get('additional_args') 182 | 183 | return args 184 | 185 | def run_command(self, args, cmd, content, window, file_path): 186 | shell = False 187 | if os.name == 'nt': 188 | shell = True 189 | 190 | self.processed = False 191 | proc = subprocess.Popen(args, shell=shell, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE) 192 | 193 | if file_path: 194 | phpcs_content = 'phpcs_input_file: ' + file_path + "\n" + content; 195 | else: 196 | phpcs_content = content; 197 | 198 | if proc.stdout: 199 | data = proc.communicate(phpcs_content.encode('utf-8'))[0] 200 | 201 | if cmd == 'phpcs': 202 | sublime.set_timeout(lambda: self.process_phpcs_results(data, window), 0) 203 | else: 204 | data = data.decode('utf-8') 205 | sublime.set_timeout(lambda: self.process_phpcbf_results(data, window, content), 0) 206 | 207 | def init_results_view(self, window): 208 | self.output_view = window.get_output_panel(RESULT_VIEW_NAME) 209 | self.output_view.set_syntax_file('Packages/Diff/Diff.tmLanguage') 210 | self.output_view.set_name(RESULT_VIEW_NAME) 211 | self.output_view.settings().set('gutter', False) 212 | 213 | self.clear_view() 214 | self.output_view.settings().set("file_path", window.active_view().file_name()) 215 | return self.output_view 216 | 217 | def show_results_view(self, window, data): 218 | if sublime.version().startswith('2'): 219 | data = data.decode('utf-8').replace('\r', '') 220 | else: 221 | if type(data) is bytes: 222 | data = data.decode('utf-8').replace('\r', '') 223 | 224 | outputView = self.init_results_view(window) 225 | window.run_command("show_panel", {"panel": "output." + RESULT_VIEW_NAME}) 226 | outputView.set_read_only(False) 227 | 228 | self.output_view.run_command('set_view_content', {'data':data}) 229 | 230 | outputView.set_read_only(True) 231 | 232 | 233 | def set_status_msg(self, msg): 234 | sublime.status_message(msg) 235 | 236 | def show_loading_msg(self, msg): 237 | if self.processed == True: 238 | return 239 | 240 | msg = msg[:-2] 241 | msg = msg + ' ' + self.process_anim[sublime.platform()][self.process_anim_idx] 242 | 243 | self.process_anim_idx += 1; 244 | if self.process_anim_idx > (len(self.process_anim[sublime.platform()]) - 1): 245 | self.process_anim_idx = 0 246 | 247 | self.set_status_msg(msg) 248 | sublime.set_timeout(lambda: self.show_loading_msg(msg), 300) 249 | 250 | 251 | def clear_view(self): 252 | if self.output_view != None: 253 | self.output_view.set_read_only(False) 254 | self.output_view.run_command('set_view_content', {'data':''}) 255 | self.output_view.set_read_only(True) 256 | 257 | self.file_view.erase_regions('errors') 258 | self.file_view.erase_regions('warnings') 259 | 260 | 261 | def line_clicked(self): 262 | if self.view_type == 'phpcs': 263 | self.handle_phpcs_line_click() 264 | else: 265 | self.handle_phpcbf_line_click() 266 | 267 | 268 | def handle_phpcs_line_click(self): 269 | region = self.output_view.line(self.output_view.sel()[0]) 270 | line = self.output_view.substr(region) 271 | 272 | if line.find('[ Click here to fix this file ]') != -1: 273 | self.run(self.window, 'phpcbf', 'Runnings PHPCS Fixer ') 274 | return 275 | else: 276 | match = re.match(r'[^:0-9]+([0-9]+)\s*:', line) 277 | if not match: 278 | return 279 | 280 | # Highlight the clicked results line. 281 | self.output_view.add_regions(RESULT_VIEW_NAME, [region], "comment", 'bookmark', sublime.DRAW_OUTLINED) 282 | 283 | lineNum = match.group(1) 284 | self.go_to_line(lineNum) 285 | 286 | 287 | def handle_phpcbf_line_click(self): 288 | pnt = self.output_view.sel()[0] 289 | region = self.output_view.line(pnt) 290 | line = self.output_view.substr(region) 291 | (row, col) = self.output_view.rowcol(pnt.begin()) 292 | 293 | offset = 0 294 | found = False 295 | while not found and row > 0: 296 | text_point = self.output_view.text_point(row, 0) 297 | line = self.output_view.substr(self.output_view.line(text_point)) 298 | if line.startswith('@@'): 299 | match = re.match(r'^@@ -\d+,\d+ \+(\d+),.*', line) 300 | if match: 301 | lineNum = int(match.group(1)) + offset - 1 302 | self.go_to_line(lineNum) 303 | 304 | break 305 | elif not line.startswith('-'): 306 | offset = offset + 1 307 | 308 | row = row - 1 309 | 310 | 311 | def go_to_line(self, lineNum): 312 | self.window.focus_view(self.file_view) 313 | self.file_view.run_command("goto_line", {"line": lineNum}) 314 | 315 | def showPopup(self, view, sublime, point): 316 | if self.output_view is None or self.output_view.window() is None: 317 | return 318 | 319 | styles = '' 322 | 323 | for region in self.regions: 324 | if region.get('region').contains(point): 325 | content = styles + '