├── .flake8 ├── .pylintrc ├── Default.sublime-commands ├── themes ├── summary-inline.html ├── below.html ├── summary-below.html └── inline.html ├── Main.sublime-menu ├── LICENSE ├── .gitignore ├── SublimeLinterInlineErrors.sublime-settings ├── README.md └── inline-errors.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=120 3 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [FORMAT] 2 | max-line-length=120 3 | 4 | [MESSAGES CONTROL] 5 | disable=C0111,E1004 -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences: SublimeLinter Settings", 4 | "command": "open_file", "args": 5 | { 6 | "file": "${packages}/SublimeLinter-inline-errors/SublimeLinterInlineErrors.sublime-settings" 7 | } 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /themes/summary-inline.html: -------------------------------------------------------------------------------- 1 | 26 |
27 | ${left_offset}
${counters}
28 |
29 | -------------------------------------------------------------------------------- /themes/below.html: -------------------------------------------------------------------------------- 1 | 32 |
33 |
${message}
34 |
35 | -------------------------------------------------------------------------------- /themes/summary-below.html: -------------------------------------------------------------------------------- 1 | 32 |
33 |
${message}
34 |
35 | -------------------------------------------------------------------------------- /themes/inline.html: -------------------------------------------------------------------------------- 1 | 32 |
33 | ${left_offset}
${message}
34 |
35 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Preferences", 4 | "id": "preferences", 5 | "mnemonic": "n", 6 | "children": 7 | [ 8 | { 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "mnemonic": "P", 12 | "children": 13 | [ 14 | { 15 | "caption": "SublimeLinter - Inline Errors", 16 | "children": 17 | [ 18 | { 19 | "caption": "Settings", 20 | "command": "edit_settings", 21 | "args": { 22 | "base_file": "${packages}/SublimeLinter Inline Errors/SublimeLinterInlineErrors.sublime-settings", 23 | "default": "{\n \n}\n" 24 | } 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | ] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Kuznetsov 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /SublimeLinterInlineErrors.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Show warning hints 3 | "show_warnings": true, 4 | 5 | // Show error hints 6 | "show_errors": true, 7 | 8 | // Show summary on top of the view 9 | "show_summary": true, 10 | 11 | // Hint font size 12 | "font_size": "0.9rem", 13 | 14 | // If true, shows hint inline, otherwise below the line 15 | "show_inline_text": true, 16 | 17 | // Hint behaviour when caret is on hinted line 18 | // 19 | // "inline" - shows inline hint 20 | // "below" - shows hint below the line 21 | // "none" - hides hint 22 | "hint_on_selected_line": "none", 23 | 24 | // Most left position for the hint in line (unless viewport is narrower than that) 25 | "min_offset": 100, 26 | 27 | // Max width for the block shown below the line 28 | "max_block_width": 80, 29 | 30 | // Minimal gap between the text line and the hint 31 | "min_gap": 5, 32 | 33 | // Theme file for inline hints 34 | "inline_theme": "Packages/SublimeLinter Inline Errors/themes/inline.html", 35 | 36 | // Theme file for below-the-line hints 37 | "below_theme": "Packages/SublimeLinter Inline Errors/themes/below.html", 38 | 39 | // Theme file for summary hints 40 | "summary_inline_theme": "Packages/SublimeLinter Inline Errors/themes/summary-inline.html", 41 | 42 | // Theme file for summary hints 43 | "summary_below_theme": "Packages/SublimeLinter Inline Errors/themes/summary-below.html", 44 | 45 | // Symbol used as a warning hint prefix 46 | // Alternative warning symbols: ❗, 🗲 47 | "warning_symbol": "⚠️", 48 | 49 | // Symbol used as an error hint prefix 50 | // Alternative errors symbols: ✖, ✘, ⮾, 🚫, 51 | "error_symbol": "⛔", 52 | 53 | // Symbol used in the offset 54 | "offset_symbol": " ", 55 | 56 | // Offset symbol color (set your background color here to hide the offset symbols) 57 | "offset_color": "553333", 58 | 59 | // Inline warning text color 60 | "inline_warning_color": "DDCC66", 61 | 62 | // Inline warning background color 63 | "inline_warning_background_color": "", 64 | 65 | // Inline error text color 66 | "inline_error_color": "DD6666", 67 | 68 | // Inline error background color 69 | "inline_error_background_color": "", 70 | 71 | // Below-the-line warning text color 72 | "below_warning_color": "FFFFFF", 73 | 74 | // Below-the-line warning background color 75 | "below_warning_background_color": "BBAA33", 76 | 77 | // Below-the-line error text color 78 | "below_error_color": "FFFFFF", 79 | 80 | // Below-the-line error background color 81 | "below_error_background_color": "993333", 82 | 83 | // Summary text color 84 | "summary_color": "FFFFFF", 85 | 86 | // Summary background color 87 | "summary_background_color": "993333", 88 | 89 | // Maximum number of words in inline hint 90 | "inline_max_words": 30, 91 | 92 | // Prints debug messages in console 93 | "debug": false 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
This is an experimental package and apparently it does not work very well. Don't expect much of it.

If you still need decent error displaying (I bet you do), consider using SublimeLinter v4
2 | 3 | # SublimeLinter-inline-errors 4 | Shows linting errors inline with Phantom API 5 | 6 | Much experimental. Very beta. 7 | 8 | Works like this: 9 | 10 | ![](https://media.giphy.com/media/xT39CTcPGpMUcVKHQs/giphy.gif) 11 | 12 | Or this: 13 | 14 | ![](https://media.giphy.com/media/l41JK9BsUAhlWLB6M/giphy.gif) 15 | 16 | ## Installation 17 | 18 | - Open `Package Control: Install Package` 19 | - Find and install `SublimeLinter Inline Errors` package 20 | 21 | ## Settings 22 | 23 | ```js 24 | { 25 | // Show warning hints 26 | "show_warnings": true, 27 | 28 | // Show error hints 29 | "show_errors": true, 30 | 31 | // Show summary on top of the view 32 | "show_summary": true, 33 | 34 | // Hint font size 35 | "font_size": "0.9rem", 36 | 37 | // If true, shows hint inline, otherwise below the line 38 | "show_inline_text": true, 39 | 40 | // Hint behaviour when caret is on hinted line 41 | // 42 | // "inline" - shows inline hint 43 | // "below" - shows hint below the line 44 | // "none" - hides hint 45 | "hint_on_selected_line": "none", 46 | 47 | // Most left position for the hint in line (unless viewport is narrower than that) 48 | "min_offset": 100, 49 | 50 | // Max width for the block shown below the line 51 | "max_block_width": 80, 52 | 53 | // Minimal gap between the text line and the hint 54 | "min_gap": 5, 55 | 56 | // Theme file for inline hints 57 | "inline_theme": "Packages/SublimeLinter Inline Errors/themes/inline.html", 58 | 59 | // Theme file for below-the-line hints 60 | "below_theme": "Packages/SublimeLinter Inline Errors/themes/below.html", 61 | 62 | // Theme file for summary hints 63 | "summary_inline_theme": "Packages/SublimeLinter Inline Errors/themes/summary-inline.html", 64 | 65 | // Theme file for summary hints 66 | "summary_below_theme": "Packages/SublimeLinter Inline Errors/themes/summary-below.html", 67 | 68 | // Symbol used as a warning hint prefix 69 | "warning_symbol": "⚠️", 70 | 71 | // Symbol used as an error hint prefix 72 | "error_symbol": "⛔", 73 | 74 | // Symbol used in the offset 75 | "offset_symbol": " ", 76 | 77 | // Offset symbol color (set your background color here to hide the offset symbols) 78 | "offset_color": "553333", 79 | 80 | // Inline warning text color 81 | "inline_warning_color": "DDCC66", 82 | 83 | // Inline warning background color 84 | "inline_warning_background_color": "", 85 | 86 | // Inline error text color 87 | "inline_error_color": "DD6666", 88 | 89 | // Inline error background color 90 | "inline_error_background_color": "", 91 | 92 | // Below-the-line warning text color 93 | "below_warning_color": "FFFFFF", 94 | 95 | // Below-the-line warning background color 96 | "below_warning_background_color": "BBAA33", 97 | 98 | // Below-the-line error text color 99 | "below_error_color": "FFFFFF", 100 | 101 | // Below-the-line error background color 102 | "below_error_background_color": "993333", 103 | 104 | // Summary text color 105 | "summary_color": "FFFFFF", 106 | 107 | // Summary background color 108 | "summary_background_color": "993333", 109 | 110 | // Maximum number of words in inline hint 111 | "inline_max_words": 30, 112 | 113 | // Prints debug messages in console 114 | "debug": false 115 | } 116 | ``` 117 | 118 | # Known issues 119 | - By default, inline hint is hidden for a current line, since Phantom window can mess up with your code while you editing it. If you feel lucky, you can always show inline hint by setting `hint_on_selected_line: "inline"` 120 | - If you click between code line and the hint, nothing happens (it should put cursor at the end of a line). It's a bit annoying. Unfortunately, there's no way to make phantom transparent for pointer events (so the click would be handled by the editor), making a hidden link there to handle the click manually also doesn't work well. 121 | -------------------------------------------------------------------------------- /inline-errors.py: -------------------------------------------------------------------------------- 1 | import re 2 | from cgi import escape 3 | from string import Template 4 | import textwrap 5 | import sublime 6 | import sublime_plugin 7 | from SublimeLinter.sublimelinter import SublimeLinter as Linter 8 | from SublimeLinter.lint import persist, highlight 9 | 10 | DEBUG = None 11 | 12 | PHANTOM_SETS_BY_BUFFER = {} 13 | SUMMARY_PHANTOM_SETS_BY_BUFFER = {} 14 | 15 | 16 | class InlineErrorSettings: 17 | show_summary = None 18 | show_warnings = None 19 | show_errors = None 20 | inline_theme = None 21 | below_theme = None 22 | summary_inline_theme = None 23 | summary_below_theme = None 24 | hint_on_selected_line = None 25 | min_offset = None 26 | max_block_width = None 27 | min_gap = None 28 | show_inline_text = None 29 | warning_symbol = None 30 | error_symbol = None 31 | offset_symbol = None 32 | offset_color = None 33 | inline_warning_color = None 34 | inline_warning_background_color = None 35 | inline_error_color = None 36 | inline_error_background_color = None 37 | below_warning_color = None 38 | below_warning_background_color = None 39 | below_error_color = None 40 | below_error_background_color = None 41 | summary_color = None 42 | summary_background_color = None 43 | inline_max_words = None 44 | font_size = None 45 | debug = None 46 | 47 | def __init__(self, on_change=None): 48 | s = sublime.load_settings('SublimeLinterInlineErrors.sublime-settings') 49 | fields = [f for f in dir(self) if not f.startswith('__')] 50 | for f in fields: 51 | setattr(self, f, s.get(f)) 52 | if on_change: 53 | s.add_on_change('on_change_callback', on_change) 54 | 55 | 56 | def print_debug(*args): 57 | global DEBUG 58 | if DEBUG is None: 59 | DEBUG = InlineErrorSettings().debug 60 | if DEBUG: 61 | 'invalid syntax; SyntaxError' 62 | print('[INLINE ERRORS]', *args) 63 | 64 | 65 | def plugin_loaded(): 66 | global PHANTOM_SETS_BY_BUFFER 67 | global SUMMARY_PHANTOM_SETS_BY_BUFFER 68 | print_debug('Clear all phantoms') 69 | for _, phantom_set in PHANTOM_SETS_BY_BUFFER.items(): 70 | phantom_set.update([]) 71 | for _, phantom_set in SUMMARY_PHANTOM_SETS_BY_BUFFER.items(): 72 | phantom_set.update([]) 73 | 74 | 75 | def plugin_unloaded(): 76 | global PHANTOM_SETS_BY_BUFFER 77 | global SUMMARY_PHANTOM_SETS_BY_BUFFER 78 | print_debug('Clear all phantoms') 79 | for _, phantom_set in PHANTOM_SETS_BY_BUFFER.items(): 80 | phantom_set.update([]) 81 | for _, phantom_set in SUMMARY_PHANTOM_SETS_BY_BUFFER.items(): 82 | phantom_set.update([]) 83 | 84 | 85 | class InlineErrors(sublime_plugin.ViewEventListener): 86 | _expanded_error_line = None 87 | linter = None 88 | _settings = None 89 | _current_line = -1 90 | _expand_summary = False 91 | 92 | def __init__(self, *args, **kwargs): 93 | super().__init__(*args, **kwargs) 94 | 95 | self.linter = Linter.shared_instance 96 | 97 | old_highlight = Linter.highlight 98 | _self = self 99 | 100 | def _highlight(self, view, linters, hit_time): 101 | res = old_highlight(self, view, linters, hit_time) 102 | _self.update_phantoms(force=True) 103 | return res 104 | Linter.highlight = _highlight 105 | 106 | old_clear = Linter.clear 107 | 108 | def _clear(self, view): 109 | res = old_clear(self, view) 110 | _self.clear(view) 111 | return res 112 | Linter.clear = _clear 113 | 114 | def settings(self): 115 | if self._settings is None: 116 | self._settings = InlineErrorSettings( 117 | self.on_settings_change.__get__(self, InlineErrors) 118 | ) 119 | return self._settings 120 | 121 | def on_settings_change(self): 122 | self._settings = None 123 | self.update_phantoms(force=True) 124 | 125 | def on_selection_modified(self): 126 | self.update_phantoms() 127 | 128 | def get_template(self, theme_path): 129 | s = self.settings() 130 | 131 | if theme_path == 'none' or theme_path is None: 132 | return None 133 | 134 | tooltip_text = sublime.load_resource(theme_path) 135 | 136 | return Template(tooltip_text) 137 | 138 | def get_current_line(self): 139 | try: 140 | return self.view.rowcol(self.view.sel()[0].begin())[0] 141 | except IndexError: 142 | return -1 143 | 144 | def update_phantoms(self, force=False): 145 | linter = self.linter 146 | 147 | view = self.view 148 | 149 | if linter.is_scratch(view): 150 | return 151 | 152 | lineno = self.get_current_line() 153 | 154 | if not force and lineno == self._current_line: 155 | return 156 | 157 | self._current_line = lineno 158 | 159 | vid = view.id() 160 | 161 | if vid in persist.errors: 162 | errors = persist.errors[vid] 163 | 164 | self.show_phantoms(view, errors, lineno, persist.highlights[vid].all) 165 | 166 | def get_phantom_set(self, view): 167 | global PHANTOM_SETS_BY_BUFFER 168 | 169 | buffer_id = view.buffer_id() 170 | if buffer_id not in PHANTOM_SETS_BY_BUFFER: 171 | phantom_set = sublime.PhantomSet(view, 'linter-inline-errors') 172 | PHANTOM_SETS_BY_BUFFER[buffer_id] = phantom_set 173 | else: 174 | phantom_set = PHANTOM_SETS_BY_BUFFER[buffer_id] 175 | 176 | return phantom_set 177 | 178 | def get_summary_phantom_set(self, view): 179 | global SUMMARY_PHANTOM_SETS_BY_BUFFER 180 | 181 | buffer_id = view.buffer_id() 182 | if buffer_id not in SUMMARY_PHANTOM_SETS_BY_BUFFER: 183 | phantom_set = sublime.PhantomSet(view, 'linter-inline-errors-summary') 184 | SUMMARY_PHANTOM_SETS_BY_BUFFER[buffer_id] = phantom_set 185 | else: 186 | phantom_set = SUMMARY_PHANTOM_SETS_BY_BUFFER[buffer_id] 187 | 188 | return phantom_set 189 | 190 | def show_phantoms(self, view, errors, selected_line, highlights): 191 | s = self.settings() 192 | 193 | templates = { 194 | 'inline': self.get_template(s.inline_theme), 195 | 'below': self.get_template(s.below_theme) 196 | } 197 | 198 | if templates['inline'] is None or templates['below'] is None: 199 | return 200 | 201 | phantom_set = self.get_phantom_set(view) 202 | 203 | filtered_errors = self.filter_errors(errors, highlights) 204 | 205 | phantoms = [ 206 | self.get_phantoms(line, line_errors, templates, view, selected_line == line) 207 | for (line, line_errors) in filtered_errors 208 | ] 209 | print_debug('Update phantoms: %s' % len(phantoms)) 210 | phantom_set.update([p for pair in phantoms for p in pair if p]) 211 | 212 | summary_phantom_set = self.get_summary_phantom_set(view) 213 | if s.show_summary and len(filtered_errors) > 0: 214 | summary_phantoms = self.get_summary_phantoms(filtered_errors) 215 | summary_phantom_set.update(summary_phantoms) 216 | else: 217 | summary_phantom_set.update([]) 218 | 219 | def filter_errors(self, errors, highlights): 220 | s = self.settings() 221 | 222 | def filter_line_errors(line_errors, line): 223 | line_errors = sorted(line_errors, key=lambda error: error[0]) 224 | line_errors = [(text, self.is_error(line, col, highlights)) for (col, text) in line_errors] 225 | 226 | if not s.show_warnings: 227 | line_errors = [(text, is_error) for (text, is_error) in line_errors if is_error] 228 | 229 | if not s.show_errors: 230 | line_errors = [(text, is_error) for (text, is_error) in line_errors if not is_error] 231 | 232 | return line_errors 233 | 234 | return [ 235 | (line, filter_line_errors(errs, line)) for (line, errs) in errors.items() 236 | ] 237 | 238 | def clear(self, view): 239 | print_debug('Clear phantoms') 240 | view.erase_phantoms('linter-inline-errors') 241 | view.erase_phantoms('linter-inline-errors-summary') 242 | 243 | def is_error(self, row, col, highlights): 244 | pos = self.view.text_point(row, col) 245 | for h in highlights: 246 | if len([e for e in h.marks['error'] if e.a == pos]) > 0: 247 | return True 248 | 249 | return False 250 | 251 | def get_summary_phantoms(self, errors): 252 | s = self.settings() 253 | 254 | flatten_errors = [ 255 | (line, text, is_error) 256 | for (line, line_errors) in errors 257 | for (text, is_error) in line_errors 258 | ] 259 | warnings_count = len([True for (_, _, is_error) in flatten_errors if not is_error]) 260 | errors_count = len([True for (_, _, is_error) in flatten_errors if is_error]) 261 | 262 | counters_message = [ 263 | (('%s %s warnings' if warnings_count > 1 else '%s %s warning') % (s.warning_symbol, warnings_count) 264 | if warnings_count > 0 else ''), 265 | (('%s %s errors' if errors_count > 1 else '%s %s error') % (s.error_symbol, errors_count) 266 | if errors_count > 0 else '') 267 | ] 268 | counters_message = '; '.join([m for m in counters_message if m]) 269 | 270 | summary_inline_template = self.get_template(s.summary_inline_theme) 271 | summary_below_template = self.get_template(s.summary_below_theme) 272 | 273 | if summary_inline_template is None or summary_below_template is None: 274 | return [] 275 | 276 | counters_html = '%s' % ('toggle_summary', counters_message) 277 | 278 | if not self._expand_summary: 279 | errors_html = '' 280 | else: 281 | errors_html = ''.join([ 282 | '%s: %s' % ( 283 | line, 'summary_error' if is_error else 'summary_warning', line, t) 284 | for (line, text, is_error) in flatten_errors for t in self.wrap_text(text, is_error) 285 | ]) 286 | 287 | region = self.view.line(self.view.text_point(0, 0)) 288 | left_offset = self.get_left_offset(region, fit_text=counters_message) 289 | 290 | inline_content = summary_inline_template.substitute( 291 | counters=counters_html, 292 | font_size=s.font_size, 293 | left_offset='
%s
' % (s.offset_symbol * left_offset), 294 | offset_color=s.offset_color, 295 | counters_background_color=( 296 | 'background-color: #%s;' % s.summary_background_color 297 | if s.summary_background_color else '' 298 | ), 299 | counters_color='color: #%s;' % s.summary_color if s.summary_color else '' 300 | ) 301 | 302 | inline_phantom = sublime.Phantom( 303 | sublime.Region(region.b, region.b), 304 | inline_content, 305 | sublime.LAYOUT_INLINE, 306 | on_navigate=self.on_summary_navigate.__get__(self, InlineErrors) 307 | ) 308 | 309 | if self._expand_summary: 310 | below_content = summary_below_template.substitute( 311 | message=errors_html, 312 | font_size=s.font_size, 313 | warning_background_color=( 314 | 'background-color: #%s;' % s.below_warning_background_color 315 | if s.below_warning_background_color else '' 316 | ), 317 | warning_color='color: #%s;' % s.below_warning_color if s.below_warning_color else '', 318 | error_background_color=( 319 | 'background-color: #%s;' % s.below_error_background_color 320 | if s.below_error_background_color else '' 321 | ), 322 | error_color='color: #%s;' % s.below_error_color if s.below_error_color else '' 323 | ) 324 | 325 | line_text = self.view.substr(region) 326 | match = re.search(r'[^\s]', line_text) 327 | line_offset = (region.a + match.start()) if match else region.b 328 | 329 | below_phantom = sublime.Phantom( 330 | sublime.Region(line_offset, line_offset), 331 | below_content, 332 | sublime.LAYOUT_BELOW, 333 | on_navigate=self.on_summary_navigate.__get__(self, InlineErrors) 334 | ) 335 | 336 | return [inline_phantom, below_phantom] 337 | 338 | return [inline_phantom] 339 | 340 | def on_summary_navigate(self, text): 341 | if text == 'toggle_summary': 342 | self._expand_summary = not self._expand_summary 343 | self.update_phantoms(force=True) 344 | else: 345 | line = int(text) 346 | self.view.show(self.view.text_point(line, 0)) 347 | 348 | def wrap_text(self, text, is_error): 349 | s = self.settings() 350 | hint_symbol = s.error_symbol if is_error else s.warning_symbol 351 | wrapped = textwrap.wrap(text, s.max_block_width, break_long_words=False) 352 | text_lines = [ 353 | ('%s %s' % (hint_symbol, escape(l)) 354 | if idx == 0 else '
%s
' % escape(l)) 355 | for (idx, l) in enumerate(wrapped) 356 | ] 357 | 358 | return text_lines 359 | 360 | def get_viewport_width(self): 361 | return int(self.view.viewport_extent()[0] / self.view.em_width()) - 3 362 | 363 | def get_left_offset(self, region, fit_text=None): 364 | s = self.settings() 365 | line_width = region.b - region.a 366 | left_offset = max(s.min_offset - line_width, s.min_gap) 367 | 368 | offset_overflow = line_width + left_offset - self.get_viewport_width() + 4 369 | if fit_text is not None: 370 | offset_overflow = offset_overflow + len(fit_text) 371 | if offset_overflow > 0: 372 | left_offset = max(0, left_offset - offset_overflow) 373 | 374 | return left_offset 375 | 376 | def get_phantoms(self, line, line_errors, templates, view, is_selected): 377 | s = self.settings() 378 | 379 | region = view.line(view.text_point(line, 0)) 380 | line_width = region.b - region.a 381 | left_offset = self.get_left_offset(region) 382 | is_expanded = line == self._expanded_error_line or s.hint_on_selected_line == 'below' and is_selected 383 | 384 | if s.hint_on_selected_line == 'none' and is_selected and not is_expanded: 385 | return (None, None) 386 | 387 | has_inline_text = s.show_inline_text and not is_expanded and ( 388 | not is_selected or s.hint_on_selected_line == 'inline' 389 | ) 390 | inline_text = '; '.join([l for (l, is_error) in line_errors]) 391 | 392 | if s.inline_max_words: 393 | inline_text_words = inline_text.split(' ') 394 | if len(inline_text_words) > s.inline_max_words: 395 | inline_text = '%s…' % ' '.join(inline_text_words[:s.inline_max_words]) 396 | 397 | viewport_width = self.get_viewport_width() 398 | 399 | if is_selected: 400 | hint_overflow = (region.b - region.a) + left_offset + len(inline_text) - viewport_width + 4 401 | if hint_overflow > 0: 402 | fixed_width = len(inline_text) - hint_overflow - 1 403 | inline_text = '%s…' % inline_text[:fixed_width] if fixed_width > 0 else '' 404 | 405 | has_error = len([is_error for (l, is_error) in line_errors if is_error]) > 0 406 | hint_symbol = s.error_symbol if has_error else s.warning_symbol 407 | classname = 'inline_error' if has_error else 'inline_warning' 408 | inline_message = ( 409 | '%s %s' % (line, classname, hint_symbol, escape(inline_text)) 410 | if has_inline_text else '' 411 | ) 412 | below_message = ''.join([ 413 | '%s' % (line, 'below_error' if is_error else 'below_warning', l) 414 | for (text, is_error) in line_errors for l in self.wrap_text(text, is_error) 415 | ]) 416 | 417 | line_text = view.substr(region) 418 | match = re.search(r'[^\s]', line_text) 419 | line_offset = (region.a + match.start()) if match else region.b 420 | 421 | inline_tooltip_content = templates['inline'].substitute( 422 | line=line, 423 | left_offset='
%s
' % (s.offset_symbol * left_offset), 424 | message='%s' % (line, hint_symbol) if not has_inline_text else inline_message, 425 | font_size=s.font_size, 426 | offset_color=s.offset_color, 427 | warning_background_color=( 428 | 'background-color: #%s;' % s.inline_warning_background_color 429 | if s.inline_warning_background_color else '' 430 | ), 431 | warning_color='color: #%s;' % s.inline_warning_color if s.inline_warning_color else '', 432 | error_background_color=( 433 | 'background-color: #%s;' % s.inline_error_background_color 434 | if s.inline_error_background_color else '' 435 | ), 436 | error_color='color: #%s;' % s.inline_error_color if s.inline_error_color else '' 437 | ) 438 | 439 | below_tooltip_content = templates['below'].substitute( 440 | line=line, 441 | left_offset='', 442 | message=below_message, 443 | font_size=s.font_size, 444 | warning_background_color=( 445 | 'background-color: #%s;' % s.below_warning_background_color 446 | if s.below_warning_background_color else '' 447 | ), 448 | error_background_color=( 449 | 'background-color: #%s;' % s.below_error_background_color 450 | if s.below_error_background_color else '' 451 | ), 452 | warning_color='color: #%s;' % s.below_warning_color if s.below_warning_color else '', 453 | error_color='color: #%s;' % s.below_error_color if s.below_error_color else '' 454 | ) 455 | 456 | inline_phantom = sublime.Phantom( 457 | sublime.Region(region.b, region.b), 458 | inline_tooltip_content, 459 | sublime.LAYOUT_INLINE, 460 | on_navigate=self.on_navigate.__get__(self, InlineErrors) 461 | ) 462 | 463 | below_phantom = sublime.Phantom( 464 | sublime.Region(line_offset, line_offset), 465 | below_tooltip_content, 466 | sublime.LAYOUT_BELOW, 467 | on_navigate=self.on_navigate.__get__(self, InlineErrors) 468 | ) 469 | 470 | return (inline_phantom, below_phantom if is_expanded else None) 471 | 472 | def on_navigate(self, text): 473 | if text == 'margin': 474 | sublime.set_timeout(self.set_cursor.__get__(self, InlineErrors), 100) 475 | return 476 | 477 | line = int(text) 478 | 479 | self._expanded_error_line = line if self._expanded_error_line != line else None 480 | 481 | self.update_phantoms(force=True) 482 | 483 | def set_cursor(self): 484 | pass 485 | # self.view.sel().clear() 486 | # self.view.sel().add(sublime.Region(0, 0)) 487 | 488 | def on_activated_async(self): 489 | self.update_phantoms() 490 | --------------------------------------------------------------------------------