diff --git a/RustEnhanced.sublime-commands b/RustEnhanced.sublime-commands index 68eac02a..2ba0b823 100644 --- a/RustEnhanced.sublime-commands +++ b/RustEnhanced.sublime-commands @@ -56,4 +56,8 @@ "caption": "Rust: Open Debug Log", "command": "rust_open_log" }, + { + "caption": "Rust: Popup Message At Cursor", + "command": "rust_message_popup" + } ] diff --git a/RustEnhanced.sublime-settings b/RustEnhanced.sublime-settings index 1bbc9dc8..32b13dcf 100644 --- a/RustEnhanced.sublime-settings +++ b/RustEnhanced.sublime-settings @@ -37,6 +37,9 @@ // For errors/warnings, how to highlight the region of the error. // "outline" - Outlines the region. + // "solid_underline" - A solid underline. + // "stippled_underline" - A stippled underline. + // "squiggly_underline" - A squiggly underline. // "none" - No outlining. "rust_region_style": "outline", @@ -51,6 +54,9 @@ // "solid" - Solid background color. "rust_message_theme": "clear", + // If `true`, displays diagnostic messages under the cursor in the status bar. + "rust_message_status_bar": false, + // If your cargo project has several build targets, it's possible to specify mapping of // source code filenames to the target names to enable syntax checking. // "projects": { diff --git a/SyntaxCheckPlugin.py b/SyntaxCheckPlugin.py index baeaeedd..3a0a32e7 100755 --- a/SyntaxCheckPlugin.py +++ b/SyntaxCheckPlugin.py @@ -1,9 +1,9 @@ import sublime import sublime_plugin import os +import time from .rust import (messages, rust_proc, rust_thread, util, target_detect, cargo_settings, semver, log) -from pprint import pprint """On-save syntax checking. @@ -13,26 +13,31 @@ """ +# TODO: Use ViewEventListener if +# https://github.com/SublimeTextIssues/Core/issues/2411 is fixed. class RustSyntaxCheckEvent(sublime_plugin.EventListener): - # Beware: This gets called multiple times if the same buffer is opened in - # multiple views (with the same view passed in each time). See: - # https://github.com/SublimeTextIssues/Core/issues/289 + last_save = 0 + def on_post_save(self, view): - # Are we in rust scope and is it switched on? - # We use phantoms which were added in 3118 - if int(sublime.version()) < 3118: + enabled = util.get_setting('rust_syntax_checking', True) + if not enabled or not util.active_view_is_rust(view=view): + return + prev_save = self.last_save + self.last_save = time.time() + if self.last_save - prev_save < 0.25: + # This is a guard for a few issues. + # * `on_post_save` gets called multiple times if the same buffer + # is opened in multiple views (with the same view passed in each + # time). See: + # https://github.com/SublimeTextIssues/Core/issues/289 + # * When using "Save All" we want to avoid launching a bunch of + # threads and then immediately killing them. return log.clear_log(view.window()) - - enabled = util.get_setting('rust_syntax_checking', True) - if enabled and util.active_view_is_rust(view=view): - t = RustSyntaxCheckThread(view) - t.start() - elif not enabled: - # If the user has switched OFF the plugin, remove any phantom - # lines. - messages.clear_messages(view.window()) + messages.erase_status(view) + t = RustSyntaxCheckThread(view) + t.start() class RustSyntaxCheckThread(rust_thread.RustThread, rust_proc.ProcListener): diff --git a/cargo_build.py b/cargo_build.py index 6bba0615..c2fc49c8 100644 --- a/cargo_build.py +++ b/cargo_build.py @@ -222,32 +222,22 @@ def run(self): ON_LOAD_MESSAGES_ENABLED = True -class CargoEventListener(sublime_plugin.EventListener): +class MessagesViewEventListener(sublime_plugin.ViewEventListener): """Every time a new file is loaded, check if is a Rust file with messages, and if so, display the messages. """ - def on_load(self, view): - if ON_LOAD_MESSAGES_ENABLED and util.active_view_is_rust(view=view): - # For some reason, view.window() returns None here. - # Use set_timeout to give it time to attach to a window. - sublime.set_timeout( - lambda: messages.show_messages_for_view(view), 1) + @classmethod + def is_applicable(cls, settings): + return ON_LOAD_MESSAGES_ENABLED and util.is_rust_view(settings) - def on_query_context(self, view, key, operator, operand, match_all): - # Used by the Escape-key keybinding to dismiss inline phantoms. - if key == 'rust_has_messages': - try: - winfo = messages.WINDOW_MESSAGES[view.window().id()] - has_messages = not winfo['hidden'] - except KeyError: - has_messages = False - if operator == sublime.OP_EQUAL: - return operand == has_messages - elif operator == sublime.OP_NOT_EQUAL: - return operand != has_messages - return None + @classmethod + def applies_to_primary_view_only(cls): + return False + + def on_load_async(self): + messages.show_messages_for_view(self.view) class NextPrevBase(sublime_plugin.WindowCommand): @@ -486,15 +476,79 @@ class CargoMessageHover(sublime_plugin.ViewEventListener): @classmethod def is_applicable(cls, settings): - s = settings.get('syntax') - package_name = __package__.split('.')[0] - return s == 'Packages/%s/RustEnhanced.sublime-syntax' % (package_name,) + return util.is_rust_view(settings) + + @classmethod + def applies_to_primary_view_only(cls): + return False def on_hover(self, point, hover_zone): if util.get_setting('rust_phantom_style', 'normal') == 'popup': messages.message_popup(self.view, point, hover_zone) +class RustMessagePopupCommand(sublime_plugin.TextCommand): + + """Manually display a popup for any message under the cursor.""" + + def run(self, edit): + for r in self.view.sel(): + messages.message_popup(self.view, r.begin(), sublime.HOVER_TEXT) + + +class RustMessageStatus(sublime_plugin.ViewEventListener): + + """Display message under cursor in status bar.""" + + @classmethod + def is_applicable(cls, settings): + return (util.is_rust_view(settings) + and util.get_setting('rust_message_status_bar', False)) + + @classmethod + def applies_to_primary_view_only(cls): + return False + + def on_selection_modified_async(self): + # https://github.com/SublimeTextIssues/Core/issues/289 + # Only works with the primary view, get the correct view. + # (Also called for each view, unfortunately.) + active_view = self.view.window().active_view() + if active_view and active_view.buffer_id() == self.view.buffer_id(): + view = active_view + else: + view = self.view + messages.update_status(view) + + +class RustEventListener(sublime_plugin.EventListener): + + def on_activated_async(self, view): + # This is a workaround for this bug: + # https://github.com/SublimeTextIssues/Core/issues/2411 + # It would be preferable to use ViewEventListener, but it doesn't work + # on duplicate views created with Goto Anything. + if not util.active_view_is_rust(view=view): + return + if util.get_setting('rust_message_status_bar', False): + messages.update_status(view) + messages.draw_regions_if_missing(view) + + def on_query_context(self, view, key, operator, operand, match_all): + # Used by the Escape-key keybinding to dismiss inline phantoms. + if key == 'rust_has_messages': + try: + winfo = messages.WINDOW_MESSAGES[view.window().id()] + has_messages = not winfo['hidden'] + except KeyError: + has_messages = False + if operator == sublime.OP_EQUAL: + return operand == has_messages + elif operator == sublime.OP_NOT_EQUAL: + return operand != has_messages + return None + + class RustAcceptSuggestedReplacement(sublime_plugin.TextCommand): """Used for suggested replacements issued by the compiler to apply the diff --git a/docs/img/region_style_none.png b/docs/img/region_style_none.png index 35063747..26ba5191 100644 Binary files a/docs/img/region_style_none.png and b/docs/img/region_style_none.png differ diff --git a/docs/img/region_style_outline.png b/docs/img/region_style_outline.png index 3d7e16f9..b07ef6bc 100644 Binary files a/docs/img/region_style_outline.png and b/docs/img/region_style_outline.png differ diff --git a/docs/img/region_style_solid_underline.png b/docs/img/region_style_solid_underline.png new file mode 100644 index 00000000..3cc4ae67 Binary files /dev/null and b/docs/img/region_style_solid_underline.png differ diff --git a/docs/img/region_style_squiggly_underline.png b/docs/img/region_style_squiggly_underline.png new file mode 100644 index 00000000..432b83a0 Binary files /dev/null and b/docs/img/region_style_squiggly_underline.png differ diff --git a/docs/img/region_style_stippled_underline.png b/docs/img/region_style_stippled_underline.png new file mode 100644 index 00000000..82533c40 Binary files /dev/null and b/docs/img/region_style_stippled_underline.png differ diff --git a/docs/messages.md b/docs/messages.md index 32b2f3c8..ee0f15cf 100644 --- a/docs/messages.md +++ b/docs/messages.md @@ -40,6 +40,18 @@ hovers over an error (either the gutter icon or the error outline). The +### Popup Command +You can bind the `rust_message_popup` command to a keyboard shortcut to force +a popup to open if there is a message under the cursor. Example: + +```json +{"keys": ["f8"], "command": "rust_message_popup", "context": + [ + {"key": "selector", "operator":"equal", "operand": "source.rust"} + ] +} +``` + ## Phantom Themes The style of the phantom messages is controlled with the `rust_message_theme` @@ -81,6 +93,9 @@ outline. | Value | Example | Description | | :---- | :------ | :---------- | | `outline` | | Regions are highlighted with an outline. | +| `solid_underline` | | Solid underline. | +| `stippled_underline` | | Stippled underline. | +| `squiggly_underline` | | Squiggly underline. | | `none` | | Regions are not highlighted. | ## Gutter Images @@ -105,3 +120,4 @@ A few other settings are available for controlling messages: | :------ | :------ | :---------- | | `show_panel_on_build` | `true` | If true, an output panel is displayed at the bottom of the window showing the compiler output. | | `rust_syntax_hide_warnings` | `false` | If true, will not display warning messages. | +| `rust_message_status_bar` | `false` | If true, will display the message under the cursor in the window status bar. | diff --git a/rust/batch.py b/rust/batch.py index 8d5620c4..a7e7f9d7 100644 --- a/rust/batch.py +++ b/rust/batch.py @@ -1,5 +1,7 @@ """Classes used for aggregating messages that are on the same line.""" +from . import util + class MessageBatch: @@ -48,8 +50,8 @@ def _dismiss(self, window): # (user has to close and reopen the file). I don't know of any good # workarounds. for msg in self: - view = window.find_open_file(msg.path) - if view: + views = util.open_views_for_file(window, msg.path) + for view in views: view.erase_regions(msg.region_key) view.erase_phantoms(msg.region_key) diff --git a/rust/messages.py b/rust/messages.py index 151e6b8a..540e84b1 100644 --- a/rust/messages.py +++ b/rust/messages.py @@ -221,8 +221,8 @@ def clear_messages(window, soft=False): winfo = WINDOW_MESSAGES.pop(window.id(), {}) for path, batches in winfo.get('paths', {}).items(): - view = window.find_open_file(path) - if view: + views = util.open_views_for_file(window, path) + for view in views: for batch in batches: for msg in batch: view.erase_regions(msg.region_key) @@ -256,7 +256,18 @@ def messages_finished(window): def _draw_region_highlights(view, batch): - if util.get_setting('rust_region_style') == 'none': + region_style = util.get_setting('rust_region_style') + flags = sublime.DRAW_NO_FILL | sublime.DRAW_EMPTY + if region_style == 'none': + return + elif region_style == 'solid_underline': + flags |= sublime.DRAW_NO_OUTLINE | sublime.DRAW_SOLID_UNDERLINE + elif region_style == 'stippled_underline': + flags |= sublime.DRAW_NO_OUTLINE | sublime.DRAW_STIPPLED_UNDERLINE + elif region_style == 'squiggly_underline': + flags |= sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE + + if batch.hidden: return # Collect message regions by level. @@ -295,13 +306,11 @@ def _draw_region_highlights(view, batch): scope = 'info' icon = util.icon_path(level) for key, region in regions[level]: - _sublime_add_regions( - view, key, [region], scope, icon, - sublime.DRAW_NO_FILL | sublime.DRAW_EMPTY) + _sublime_add_regions(view, key, [region], scope, icon, flags) -def message_popup(view, point, hover_zone): - """Displays a popup if there is a message at the given point.""" +def batches_at_point(view, point, hover_zone): + """Return a list of message batches at the given point.""" try: winfo = WINDOW_MESSAGES[view.window().id()] except KeyError: @@ -334,7 +343,12 @@ def filter_point(batch): return False batches = filter(filter_point, batches) + return list(batches) + +def message_popup(view, point, hover_zone): + """Displays a popup if there is a message at the given point.""" + batches = batches_at_point(view, point, hover_zone) if batches: theme = themes.THEMES[util.get_setting('rust_message_theme')] minihtml = '\n'.join(theme.render(view, batch, for_popup=True) for batch in batches) @@ -346,6 +360,25 @@ def filter_point(batch): point, max_width=max_width, on_navigate=on_nav) +STATUS_KEY = 'rust-msg-status' + + +def update_status(view): + """Display diagnostic messages in status bar under the cursor.""" + for r in view.sel(): + batches = batches_at_point(view, r.begin(), sublime.HOVER_TEXT) + if batches: + msg = batches[0].first() + view.set_status(STATUS_KEY, msg.text) + return + view.erase_status(STATUS_KEY) + + +def erase_status(view): + """Clear the status in the message bar.""" + view.erase_status(STATUS_KEY) + + def _click_handler(view, url, hide_popup=False): if url == 'hide': clear_messages(view.window(), soft=True) @@ -578,9 +611,13 @@ def redraw_all_open_views(window): return winfo['hidden'] = False for path, batches in winfo['paths'].items(): - view = window.find_open_file(path) - if view: - show_messages_for_view(view) + views = util.open_views_for_file(window, path) + if views: + for batch in batches: + # Phantoms seem to be attached to the buffer. + _show_phantom(views[0], batch) + for view in views: + _draw_region_highlights(view, batch) def show_messages_for_view(view): @@ -597,6 +634,20 @@ def show_messages_for_view(view): _draw_region_highlights(view, batch) +def draw_regions_if_missing(view): + try: + winfo = WINDOW_MESSAGES[view.window().id()] + except KeyError: + return + if winfo['hidden']: + return + batches = winfo['paths'].get(view.file_name(), []) + msgs = itertools.chain.from_iterable(batches) + if not any((view.get_regions(msg.region_key) for msg in msgs)): + for batch in batches: + _draw_region_highlights(view, batch) + + def _ith_iter_item(d, i): return next(itertools.islice(d, i, None)) @@ -620,7 +671,7 @@ def _advance_next_message(window, levels, wrap_around=False): batches = _ith_iter_item(paths.values(), path_idx) while batch_idx < len(batches): batch = batches[batch_idx] - if _is_matching_level(levels, batch.first()): + if not batch.hidden and _is_matching_level(levels, batch.first()): current_idx = (path_idx, batch_idx) win_info['batch_index'] = current_idx return current_idx @@ -660,7 +711,7 @@ def _advance_prev_message(window, levels, wrap_around=False): batches = _ith_iter_item(paths.values(), path_idx) while batch_idx >= 0: batch = batches[batch_idx] - if _is_matching_level(levels, batch.first()): + if not batch.hidden and _is_matching_level(levels, batch.first()): current_idx = (path_idx, batch_idx) win_info['batch_index'] = current_idx return current_idx @@ -1102,10 +1153,13 @@ def _save_batches(window, batches, msg_cb): path_batches.append(batch) for i, msg in enumerate(batch): msg.region_key = 'rust-%i' % (num + i,) - view = window.find_open_file(batch.path()) - if view: - _show_phantom(view, batch) - _draw_region_highlights(view, batch) - if msg_cb: - for msg in batch: - msg_cb(msg) + if not WINDOW_MESSAGES[wid]['hidden']: + views = util.open_views_for_file(window, batch.path()) + if views: + # Phantoms seem to be attached to the buffer. + _show_phantom(views[0], batch) + for view in views: + _draw_region_highlights(view, batch) + if msg_cb: + for msg in batch: + msg_cb(msg) diff --git a/rust/util.py b/rust/util.py index 08055f9a..93c55158 100644 --- a/rust/util.py +++ b/rust/util.py @@ -2,11 +2,12 @@ import sublime import textwrap -import threading -import time import os +PACKAGE_NAME = __package__.split('.')[0] + + def index_with(l, cb): """Find the index of a value in a sequence using a callback. @@ -98,6 +99,12 @@ def active_view_is_rust(window=None, view=None): return 'source.rust' in view.scope_name(0) +def is_rust_view(settings): + """Helper for use with ViewEventListener.""" + s = settings.get('syntax') + return (s == 'Packages/%s/RustEnhanced.sublime-syntax' % (PACKAGE_NAME,)) + + def get_cargo_metadata(window, cwd, toolchain=None): """Load Cargo metadata. @@ -138,7 +145,6 @@ def icon_path(level, res=None): if level not in ('error', 'warning', 'note', 'help', 'none'): return '' gutter_style = get_setting('rust_gutter_style', 'shape') - package_name = __package__.split('.')[0] if gutter_style == 'none': return '' else: @@ -147,4 +153,13 @@ def icon_path(level, res=None): else: res_suffix = '' return 'Packages/%s/images/gutter/%s-%s%s.png' % ( - package_name, gutter_style, level, res_suffix) + PACKAGE_NAME, gutter_style, level, res_suffix) + + +def open_views_for_file(window, file_name): + """Return all views for the given file name.""" + view = window.find_open_file(file_name) + if view is None: + return [] + + return [v for v in window.views() if v.buffer_id() == view.buffer_id()] diff --git a/tests/rust_test_common.py b/tests/rust_test_common.py index 0c721d4b..3c0af6c9 100644 --- a/tests/rust_test_common.py +++ b/tests/rust_test_common.py @@ -71,6 +71,12 @@ def setUp(self): # Override settings. self._original_settings = {} self.settings = sublime.load_settings('RustEnhanced.sublime-settings') + # Ensure all settings are at defaults. + defaults = sublime.load_resource('Packages/%s/RustEnhanced.sublime-settings' % ( + util.PACKAGE_NAME,)) + defaults = sublime.decode_value(defaults) + for key, value in defaults.items(): + self._override_setting(key, value) self._override_setting('show_panel_on_build', False) self._override_setting('cargo_build', {}) # Disable incremental compilation (first enabled in 1.24). It slows