├── .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 |
35 |
--------------------------------------------------------------------------------
/themes/summary-below.html:
--------------------------------------------------------------------------------
1 |
32 |
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 | 
11 |
12 | Or this:
13 |
14 | 
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 |
--------------------------------------------------------------------------------