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