├── .gitattributes ├── fixture.js ├── messages.json ├── SublimeLinterContribXO.sublime-settings ├── screenshot.png ├── Default.sublime-commands ├── keymaps ├── Default (OSX).sublime-keymap ├── Default (Linux).sublime-keymap └── Default (Windows).sublime-keymap ├── .editorconfig ├── messages └── install.txt ├── license ├── Main.sublime-menu ├── readme.md └── linter.py /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /fixture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var foo = "bar"; 3 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.txt" 3 | } 4 | -------------------------------------------------------------------------------- /SublimeLinterContribXO.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "fix_on_save": false 3 | } 4 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xojs/SublimeLinter-contrib-xo/HEAD/screenshot.png -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "SublimeLinter XO: Fix current file", 4 | "command": "xo_fix" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /keymaps/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "xo_fix", 4 | "keys": [ 5 | "ctrl+alt+f" 6 | ] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /keymaps/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "xo_fix", 4 | "keys": [ 5 | "ctrl+alt+f" 6 | ] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /keymaps/Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "command": "xo_fix", 4 | "keys": [ 5 | "ctrl+alt+f" 6 | ] 7 | } 8 | ] 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /messages/install.txt: -------------------------------------------------------------------------------- 1 | SublimeLinter-contrib-xo 2 | ------------------------ 3 | 4 | This linter plugin for SublimeLinter provides an interface to XO. 5 | 6 | For more information, please see: 7 | https://github.com/sindresorhus/SublimeLinter-contrib-xo 8 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tools", 4 | "id": "tools", 5 | "children": [ 6 | { 7 | "id": "SublimeLinter-contrib-xo", 8 | "caption": "SublimeLinter XO", 9 | "children": [ 10 | { 11 | "caption": "Fix current file", 12 | "command": "xo_fix" 13 | } 14 | ] 15 | } 16 | ] 17 | }, 18 | { 19 | "caption": "Preferences", 20 | "id": "preferences", 21 | "children": [ 22 | { 23 | "caption": "Package Settings", 24 | "id": "package-settings", 25 | "children": [ 26 | { 27 | "caption": "SublimeLinter XO", 28 | "children": [ 29 | { 30 | "caption": "Settings - Default", 31 | "command": "open_file", 32 | "args": { 33 | "file": "${packages}/SublimeLinter-contrib-xo/SublimeLinterContribXO.sublime-settings" 34 | } 35 | }, 36 | { 37 | "caption": "Settings - User", 38 | "command": "open_file", 39 | "args": { 40 | "file": "${packages}/User/SublimeLinterContribXO.sublime-settings" 41 | } 42 | }, 43 | { "caption": "-" }, 44 | { 45 | "command": "open_file", "args": 46 | { 47 | "file": "${packages}/SublimeLinter-contrib-xo/keymaps/Default (Linux).sublime-keymap", 48 | "platform": "Linux" 49 | }, 50 | "caption": "Key Bindings - Default" 51 | }, 52 | { 53 | "command": "open_file", "args": 54 | { 55 | "file": "${packages}/User/Default (Linux).sublime-keymap", 56 | "platform": "Linux" 57 | }, 58 | "caption": "Key Bindings - User" 59 | }, 60 | { 61 | "command": "open_file", "args": 62 | { 63 | "file": "${packages}/SublimeLinter-contrib-xo/keymaps/Default (OSX).sublime-keymap", 64 | "platform": "OSX" 65 | }, 66 | "caption": "Key Bindings - Default" 67 | }, 68 | { 69 | "command": "open_file", "args": 70 | { 71 | "file": "${packages}/User/Default (OSX).sublime-keymap", 72 | "platform": "OSX" 73 | }, 74 | "caption": "Key Bindings - User" 75 | }, 76 | { 77 | "command": "open_file", "args": 78 | { 79 | "file": "${packages}/SublimeLinter-contrib-xo/keymaps/Default (Windows).sublime-keymap", 80 | "platform": "Windows" 81 | }, 82 | "caption": "Key Bindings - Default" 83 | }, 84 | { 85 | "command": "open_file", "args": 86 | { 87 | "file": "${packages}/User/Default (Windows).sublime-keymap", 88 | "platform": "Windows" 89 | }, 90 | "caption": "Key Bindings - User" 91 | } 92 | ] 93 | } 94 | ] 95 | } 96 | ] 97 | } 98 | ] 99 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # SublimeLinter-contrib-xo 2 | 3 | ![](screenshot.png) 4 | 5 | This linter plugin for [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter) provides an interface to [XO](https://github.com/xojs/xo). It will be used with files that have the “JavaScript” syntax. 6 | 7 | ## Installation 8 | 9 | SublimeLinter must be installed in order to use this plugin. 10 | 11 | Please use [Package Control](https://packagecontrol.io) to install the linter plugin. 12 | 13 | Before installing this plugin, you must ensure that `xo` is installed in your project: 14 | 15 | ```sh 16 | npm install xo 17 | ``` 18 | 19 | In order for `xo` to be executed by SublimeLinter, you must ensure that its path is available to SublimeLinter. The docs cover [troubleshooting PATH configuration](https://sublimelinter.readthedocs.io/en/latest/troubleshooting.html#finding-a-linter-executable). 20 | 21 | ## Settings 22 | 23 | - [SublimeLinter settings](https://sublimelinter.readthedocs.org/en/latest/settings.html) 24 | - [Linter settings](https://sublimelinter.readthedocs.org/en/latest/linter_settings.html) 25 | 26 | Also, you can change general plugin setting from: “Preferences › Package Settings › SublimeLinter XO”. 27 | 28 | ## Auto-fix 29 | 30 | To run the auto-fixer, press the `ctrl+alt+f` shortcut or use the menu entry “Tools › SublimeLinter XO › Fix current file”. 31 | 32 | The shortcut can be changed in “Preferences › Key Bindings” by adding the following to the array: 33 | 34 | ```json 35 | { 36 | "keys": [ 37 | "ctrl+alt+f" 38 | ], 39 | "command": "xo_fix" 40 | } 41 | ``` 42 | 43 | If you want to run the auto-fixer when saving a file, you can enable the `fix_on_save` setting: 44 | 45 | ```json 46 | { 47 | "fix_on_save": true 48 | } 49 | ``` 50 | 51 | ## Tips 52 | 53 | ### Using non-JS syntax 54 | 55 | Typical plugins for ESLint, for example, for TypeScript or Vue, should just work automatically if they're installed locally in your project (defined in the same `package.json`). 56 | 57 | For plugins not supported out-of-the-box, you may need to change the SublimeLinter [`selector` setting](http://www.sublimelinter.com/en/stable/linter_settings.html#selector) to include the correct syntax scope. For Vue, that could be: 58 | 59 | ```json 60 | { 61 | "linters": { 62 | "xo": { 63 | "selector": "text.html.vue, source.js - meta.attribute-with-value" 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### Help, `xo` doesn't lint my HTML files anymore! 70 | 71 | `xo` will only lint `*.js` files for standard, vanilla config without further plugins. Either install the [eslint-plugin-html](https://github.com/BenoitZugmeyer/eslint-plugin-html) or tweak the `selector`: 72 | 73 | ```json 74 | { 75 | "linters": { 76 | "xo": { 77 | "selector": "source.js - meta.attribute-with-value" 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | ## Note 84 | 85 | XO linting is only enabled for projects with `xo` in `devDependencies`/`dependencies` in package.json. 86 | -------------------------------------------------------------------------------- /linter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import sublime 4 | import sublime_plugin 5 | import subprocess 6 | import logging 7 | from SublimeLinter.lint import ( 8 | NodeLinter, 9 | PermanentError, 10 | linter as linter_module 11 | ) 12 | 13 | from SublimeLinter.lint.base_linter.node_linter import read_json_file 14 | 15 | logger = logging.getLogger('SublimeLinter.plugin.xo') 16 | 17 | STANDARD_SELECTOR = 'source.js' 18 | PLUGINS = { 19 | 'eslint-plugin-html': 'text.html', 20 | 'eslint-plugin-json': 'source.json', 21 | 'eslint-plugin-markdown': 'text.html.markdown', 22 | 'eslint-plugin-svelte3': 'text.html', 23 | 'eslint-plugin-vue': 'text.html.vue', 24 | '@typescript-eslint/parser': 'source.ts', 25 | } 26 | OPTIMISTIC_SELECTOR = ', '.join({STANDARD_SELECTOR} | set(PLUGINS.values())) 27 | 28 | startup_info = None 29 | if platform.system() == 'Windows': 30 | startup_info = subprocess.STARTUPINFO() 31 | startup_info.dwFlags |= subprocess.STARTF_USESHOWWINDOW 32 | 33 | settings = None 34 | 35 | def plugin_loaded(): 36 | global settings 37 | settings = sublime.load_settings('SublimeLinterContribXO.sublime-settings') 38 | 39 | class XO(NodeLinter): 40 | cmd = ('xo', '--stdin', '--reporter', 'compact', '--filename', '${file}') 41 | regex = ( 42 | r'^.+?: line (?P\d+), col (?P\d+), ' 43 | r'(?:(?PError)|(?PWarning)) - ' 44 | r'(?P.+)' 45 | r' \((?P.+)\)$' 46 | ) 47 | defaults = { 48 | 'selector': OPTIMISTIC_SELECTOR, 49 | 'disable_if_not_dependency': True 50 | } 51 | 52 | def run(self, cmd, code): 53 | self.ensure_plugin_installed() 54 | return super().run(cmd, code) 55 | 56 | def ensure_plugin_installed(self) -> bool: 57 | # If the user changed the selector, we take it as is. 58 | if self.settings['selector'] != OPTIMISTIC_SELECTOR: 59 | return True 60 | 61 | # Happy path. 62 | if self.view.match_selector(0, STANDARD_SELECTOR): 63 | return True 64 | 65 | # If we're here we must be pessimistic. 66 | 67 | # The `project_root` has the relevant `package.json` file colocated. 68 | # If we fallback to a global installation there is no `project_root`, 69 | # t.i. no auto-selector in that case as well. 70 | project_root = self.context.get('project_root') 71 | if project_root: 72 | # We still need to be careful, in case SublimeLinter deduced a `project_root` 73 | # without checking for the `package.json` explicitly. Basically, a 74 | # happy path for SublimeLinter core. 75 | manifest_file = os.path.join(project_root, 'package.json') 76 | try: 77 | manifest = read_json_file(manifest_file) 78 | except Exception: 79 | pass 80 | else: 81 | defined_plugins = PLUGINS.keys() & ( 82 | manifest.get('dependencies', {}).keys() 83 | | manifest.get('devDependencies', {}).keys() 84 | ) 85 | selector = ', '.join(PLUGINS[name] for name in defined_plugins) 86 | if selector and self.view.match_selector(0, selector): 87 | return True 88 | 89 | # Indicate an error which usually can only be solved by changing 90 | # the environment. Silently, do not notify and disturb the user! 91 | self.notify_unassign() 92 | raise PermanentError() 93 | 94 | def make_fake_linter(view): 95 | settings = linter_module.get_linter_settings(XO, view) 96 | return XO(view, settings) 97 | 98 | def xo_fix(self, view, content): 99 | if isinstance(content, str): 100 | content = content.encode() 101 | 102 | encoding = view.encoding() 103 | if encoding == 'Undefined': 104 | encoding = 'utf-8' 105 | 106 | logger.info('xo_fix → content length: %d', len(content)) 107 | logger.info('xo_fix → encoding: %s', encoding) 108 | logger.info('xo_fix → xo_env.PWD: %s', self.xo_env['PWD']) 109 | logger.info('xo_fix → xo_project_root: %s', self.xo_project_root) 110 | 111 | # TODO: Change to use `subprocess.run()` when Sublime updates Python to 3.5 or later. 112 | proc = subprocess.Popen( 113 | [self.xo_path, '--stdin', '--fix'], 114 | stdin=subprocess.PIPE, 115 | stderr=subprocess.PIPE, 116 | stdout=subprocess.PIPE, 117 | env=self.xo_env, 118 | cwd=self.xo_project_root, 119 | startupinfo=startup_info, 120 | ) 121 | stdout, stderr = proc.communicate(content) 122 | logger.info('xo_fix → stdout len: %d', len(stdout)) 123 | logger.info('xo_fix → stderr len: %d', len(stderr)) 124 | logger.info('xo_fix → stderr content: %s', stderr.decode(encoding)) 125 | logger.info('xo_fix → returncode: %d', proc.returncode) 126 | 127 | if proc.returncode != 0: 128 | sublime.message_dialog('[xo_fix ' + str(proc.returncode) + '] ' + stderr.decode(encoding)) 129 | return None 130 | 131 | return stdout.decode(encoding) 132 | 133 | class XoFixCommand(sublime_plugin.TextCommand): 134 | def is_enabled(self): 135 | logger.info('XoFixCommand → is_enabled?') 136 | linter = make_fake_linter(self.view) 137 | logger.info('XoFixCommand → project_root → %s', linter.context.get('project_root')) 138 | 139 | self.xo_start_dir = linter.get_start_dir() 140 | logger.info('XoFixCommand → xo_start_dir %s', self.xo_start_dir) 141 | if not self.xo_start_dir: 142 | logger.info('XoFixCommand → xo_start_dir → False') 143 | return False 144 | 145 | self.xo_path = linter.find_local_executable(self.xo_start_dir, 'xo') 146 | logger.info('XoFixCommand → xo_path %s', self.xo_path) 147 | if not self.xo_path: 148 | logger.info('XoFixCommand → xo_path → False') 149 | return False 150 | 151 | self.xo_project_root = linter.context.get('project_root') 152 | self.xo_env = os.environ.copy() 153 | self.xo_env['PWD'] = self.xo_project_root 154 | self.xo_env['PATH'] += os.pathsep + '/usr/local/bin' # Ensure correct PATH for Node.js on macOS 155 | 156 | logger.info('XoFixCommand → environ.path → %s', self.xo_env['PATH']) 157 | logger.info('XoFixCommand → project_root → %s', self.xo_project_root) 158 | logger.info('XoFixCommand → return → True') 159 | return True 160 | 161 | def run(self, edit): 162 | region = sublime.Region(0, self.view.size()) 163 | content = self.view.substr(region) 164 | 165 | replacement = xo_fix(self, self.view, content) 166 | if replacement != None and content != replacement: 167 | self.view.replace(edit, region, replacement) 168 | 169 | class XoFixListener(sublime_plugin.EventListener): 170 | def on_pre_save(self, view): 171 | if not settings.get('fix_on_save', False): 172 | return 173 | view.run_command('xo_fix') 174 | --------------------------------------------------------------------------------