diff --git a/README.md b/README.md
index b7213972..985f2818 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ It is possible that the built-in `Rust` package will cause conflicts with Rust E
## Features
### Go To Definition
### Cargo Build
-Rust Enhanced has a custom build system tailored for running Cargo. It will display errors and warnings in line using Sublime's phantoms. It also supports a variety of configuration options to control how Cargo is run.
+Rust Enhanced has a custom build system tailored for running Cargo. It will display errors and warnings in line using Sublime's phantoms (see [Messages](docs/messages.md) for settings to control how messages are displayed). It also supports a variety of configuration options to control how Cargo is run.

@@ -119,7 +119,7 @@ To customize the settings, use the command from the Sublime menu:
Additionally, you can customize settings per-project by adding settings to your `.sublime-project` file under the `"settings"` key.
## Development
-Development is quite simple, just check out this project to your Sublime Text 3 packages folder, and switch to using this one.
+Development is quite simple, just check out this project to your Sublime Text 3 packages folder, and switch to using this one.
Syntax definitions are defined in the `RustEnhanced.sublime-syntax` file.
## Credits
diff --git a/RustEnhanced.sublime-settings b/RustEnhanced.sublime-settings
index a248a666..b030639e 100644
--- a/RustEnhanced.sublime-settings
+++ b/RustEnhanced.sublime-settings
@@ -14,7 +14,7 @@
// If true, will not display warning messages.
"rust_syntax_hide_warnings": false,
- // Color of messages.
+ // Color of messages for "clear" theme.
// These use CSS colors. See
// https://www.sublimetext.com/docs/3/minihtml.html for more detail.
"rust_syntax_error_color": "var(--redish)",
@@ -46,6 +46,11 @@
// "none" - Do not place icons in the gutter.
"rust_gutter_style": "shape",
+ // Style for displaying inline messages. Can be:
+ // "clear" - Clear background with colors matching your color scheme.
+ // "solid" - Solid background color.
+ "rust_message_theme": "clear",
+
// 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/cargo_build.py b/cargo_build.py
index a159aa43..3f072b44 100644
--- a/cargo_build.py
+++ b/cargo_build.py
@@ -3,6 +3,7 @@
import functools
import sublime
import sublime_plugin
+import sys
from .rust import (rust_proc, rust_thread, opanel, util, messages,
cargo_settings, target_detect)
from .rust.cargo_config import *
@@ -486,3 +487,34 @@ def is_applicable(cls, settings):
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 RustAcceptSuggestedReplacement(sublime_plugin.TextCommand):
+
+ """Used for suggested replacements issued by the compiler to apply the
+ suggested replacement.
+ """
+
+ def run(self, edit, region, replacement):
+ region = sublime.Region(*region)
+ self.view.replace(edit, region, replacement)
+
+
+def plugin_unloaded():
+ messages.clear_all_messages()
+ try:
+ from package_control import events
+ except ImportError:
+ return
+ package_name = __package__.split('.')[0]
+ if events.pre_upgrade(package_name):
+ # When upgrading the package, Sublime currently does not cleanly
+ # unload the `rust` Python package. This is a workaround to ensure
+ # that it gets completely unloaded so that when it upgrades it will
+ # load the new package. See
+ # https://github.com/SublimeTextIssues/Core/issues/2207
+ re_keys = [key for key in sys.modules if key.startswith(package_name + '.rust')]
+ for key in re_keys:
+ del sys.modules[key]
+ if package_name in sys.modules:
+ del sys.modules[package_name]
diff --git a/changelog/2.11.0.md b/changelog/2.11.0.md
new file mode 100644
index 00000000..0f19517b
--- /dev/null
+++ b/changelog/2.11.0.md
@@ -0,0 +1,17 @@
+# Rust Enhanced 2.11.0
+
+You must restart Sublime after installing this update.
+
+## New Features
+- Added `"rust_message_theme"` configuration setting for choosing different
+ styles of inline messages. Currently two options are available: "clear" and
+ "solid". See
+ https://github.com/rust-lang/rust-enhanced/blob/master/docs/build.md#general-settings
+ for examples.
+
+- If the Rust compiler provides a suggestion on how to fix an error, the
+ inline messages now include a link that you can click to automatically apply
+ the suggestion.
+
+## Syntax Updates
+- Support u128/i128 integer suffix.
diff --git a/docs/build.md b/docs/build.md
index 2b296df6..c57a8b43 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -4,6 +4,9 @@ The Rust Enhanced build system provides an interface for running Cargo. It can
show inline warning and error messages. It also has a variety of ways of
configuring options for how Cargo is run.
+See [Messages](messages.md) for settings to control how compiler messages are
+displayed.
+
## Usage
When Sublime is set to use "Automatic" build system detection, it will choose
@@ -42,28 +45,6 @@ Document | cargo doc
| Builds package documentation.
Clippy | cargo clippy
| Runs [Clippy](https://github.com/Manishearth/rust-clippy). Clippy must be installed, and currently requires the nightly toolchain.
Script | cargo script $path
| Runs [Cargo Script](https://github.com/DanielKeep/cargo-script). Cargo Script must be installed. This is an addon that allows you to run a Rust source file like a script (without a Cargo.toml manifest).
-## General Settings
-
-General settings (see [Settings](../README.md#settings)) for how messages are displayed are:
-
-| Setting | Default | Description |
-| :------ | :------ | :---------- |
-| `rust_syntax_hide_warnings` | `false` | If true, will not display warning messages. |
-| `rust_syntax_error_color` | `"var(--redish)"` | Color of error messages. |
-| `rust_syntax_warning_color` | `"var(--yellowish)"` | Color of warning messages. |
-| `rust_syntax_note_color` | `"var(--greenish)"` | Color of note messages. |
-| `rust_syntax_help_color` | `"var(--bluish)"` | Color of help messages. |
-| `rust_phantom_style` | `"normal"` | How to display inline messages. Either `normal`, `popup`, or `none`. |
-| `rust_region_style` | `"outline"` | How to highlight messages. Either `outline` or `none`. |
-| `rust_gutter_style` | `"shape"` | Type of icon to show in the gutter. Either `shape`, `circle`, or `none`. |
-
-It also supports Sublime's build settings:
-
-| Setting | Default | Description |
-| :------ | :------ | :---------- |
-| `show_errors_inline` | `true` | If true, messages are displayed in line using Sublime's phantoms. If false, messages are only displayed in the output panel. |
-| `show_panel_on_build` | `true` | If true, an output panel is displayed at the bottom of the window showing the compiler output. |
-
## Cargo Settings
A variety of settings are available to customize how Cargo is run. These
diff --git a/docs/img/messages_popup.gif b/docs/img/messages_popup.gif
new file mode 100644
index 00000000..585871b9
Binary files /dev/null and b/docs/img/messages_popup.gif differ
diff --git a/docs/img/region_style_none.png b/docs/img/region_style_none.png
new file mode 100644
index 00000000..35063747
Binary files /dev/null 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
new file mode 100644
index 00000000..3d7e16f9
Binary files /dev/null and b/docs/img/region_style_outline.png differ
diff --git a/docs/img/show_errors_inline_false.png b/docs/img/show_errors_inline_false.png
new file mode 100644
index 00000000..46df5b1f
Binary files /dev/null and b/docs/img/show_errors_inline_false.png differ
diff --git a/docs/img/show_errors_inline_true.png b/docs/img/show_errors_inline_true.png
new file mode 100644
index 00000000..80af1561
Binary files /dev/null and b/docs/img/show_errors_inline_true.png differ
diff --git a/docs/img/theme_clear.png b/docs/img/theme_clear.png
new file mode 100644
index 00000000..7dd47950
Binary files /dev/null and b/docs/img/theme_clear.png differ
diff --git a/docs/img/theme_solid.png b/docs/img/theme_solid.png
new file mode 100644
index 00000000..ea5c043a
Binary files /dev/null and b/docs/img/theme_solid.png differ
diff --git a/docs/messages.md b/docs/messages.md
new file mode 100644
index 00000000..32b2f3c8
--- /dev/null
+++ b/docs/messages.md
@@ -0,0 +1,107 @@
+# Messages
+
+There are a variety of ways to display Rust compiler messages. See
+[Settings](../README.md#settings) for more details about how to configure
+settings.
+
+## Inline Phantoms vs Output Panel
+
+The `show_errors_inline` setting controls whether or not errors are shown
+inline with the code using Sublime's "phantoms". If it is `true`, it will
+also display an abbreviated message in the output panel. If it is `false`,
+messages will only be displayed in the output panel, using rustc's formatting.
+
+### `show_errors_inline`
+
+
+
+ true |
+  |
+
+
+ false |
+  |
+
+
+
+## Popup Phantom Style
+
+Phantoms can be displayed inline with the code, or as a popup when the mouse
+hovers over an error (either the gutter icon or the error outline). The
+`rust_phantom_style` setting controls this behavior.
+
+### `rust_phantom_style`
+
+| Value | Description |
+| :---- | :---------- |
+| `normal` | Phantoms are displayed inline. |
+| `popup` | Phantoms are displayed when the mouse hovers over an error. |
+| `none` | Phantoms are not displayed. |
+
+
+
+## Phantom Themes
+
+The style of the phantom messages is controlled with the `rust_message_theme`
+setting. Currently the following themes are available:
+
+### `rust_message_theme`
+
+
+
+ clear |
+  |
+
+
+ solid |
+  |
+
+
+
+### Clear Theme Colors
+
+The `clear` theme is designed to integrate with your chosen Color Scheme. You
+can customize the colors of the messages with the following settings.
+
+| Setting | Default | Description |
+| :------ | :------ | :---------- |
+| `rust_syntax_error_color` | `"var(--redish)"` | Color of error messages. |
+| `rust_syntax_warning_color` | `"var(--yellowish)"` | Color of warning messages. |
+| `rust_syntax_note_color` | `"var(--greenish)"` | Color of note messages. |
+| `rust_syntax_help_color` | `"var(--bluish)"` | Color of help messages. |
+
+
+## Region Highlighting
+
+The span of code for a compiler message is by default highlighted with an
+outline.
+
+### `rust_region_style`
+
+| Value | Example | Description |
+| :---- | :------ | :---------- |
+| `outline` |
| Regions are highlighted with an outline. |
+| `none` |
| Regions are not highlighted. |
+
+## Gutter Images
+
+The gutter (beside the line numbers) will include an icon indicating the level
+of the message. The styling of these icons is controlled with
+`rust_gutter_style`.
+
+### `rust_gutter_style`
+
+| Value | Description |
+| :---- | :---------- |
+| `shape` |
|
+| `circle` |
|
+| `none` | Do not display icons. |
+
+## Other Settings
+
+A few other settings are available for controlling messages:
+
+| Setting | Default | Description |
+| :------ | :------ | :---------- |
+| `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. |
diff --git a/images/gutter/circle-none.png b/images/gutter/circle-none.png
new file mode 100644
index 00000000..67672cbb
Binary files /dev/null and b/images/gutter/circle-none.png differ
diff --git a/images/gutter/circle-none@2x.png b/images/gutter/circle-none@2x.png
new file mode 100644
index 00000000..c9c07057
Binary files /dev/null and b/images/gutter/circle-none@2x.png differ
diff --git a/images/gutter/shape-none.png b/images/gutter/shape-none.png
new file mode 100644
index 00000000..67672cbb
Binary files /dev/null and b/images/gutter/shape-none.png differ
diff --git a/images/gutter/shape-none@2x.png b/images/gutter/shape-none@2x.png
new file mode 100644
index 00000000..c9c07057
Binary files /dev/null and b/images/gutter/shape-none@2x.png differ
diff --git a/messages.json b/messages.json
new file mode 100644
index 00000000..4935d67d
--- /dev/null
+++ b/messages.json
@@ -0,0 +1,3 @@
+{
+ "2.11.0": "changelog/2.11.0.md"
+}
diff --git a/rust/batch.py b/rust/batch.py
new file mode 100644
index 00000000..9e60da6d
--- /dev/null
+++ b/rust/batch.py
@@ -0,0 +1,118 @@
+"""Classes used for aggregating messages that are on the same line."""
+
+
+class MessageBatch:
+
+ """Abstract base class for a set of messages that apply to the same line.
+
+ :ivar children: List of additional messages, may be empty.
+ :ivar hidden: Boolean if this message should be displayed.
+ """
+
+ hidden = False
+
+ def __init__(self):
+ self.children = []
+
+ def __iter__(self):
+ """Iterates over all messages in the batch."""
+ raise NotImplementedError()
+
+ def path(self):
+ """Returns the file path of the batch."""
+ raise NotImplementedError()
+
+ def first(self):
+ """Returns the first message of the batch."""
+ raise NotImplementedError()
+
+ def dismiss(self):
+ """Permanently remove this message and all its children from the
+ view."""
+ raise NotImplementedError()
+
+ def _dismiss(self, window):
+ # There is a awkward problem with Sublime and
+ # add_regions/erase_regions. The regions are part of the undo stack,
+ # which means even after we erase them, they can come back from the
+ # dead if the user hits undo. We simply mark these as "hidden" to
+ # ensure that `clear_messages` can erase any of these zombie regions.
+ # See https://github.com/SublimeTextIssues/Core/issues/1121
+ # This is imperfect, since the user could do the following:
+ # 1) Build 2) Type some text 3) Clear Messages 4) Undo
+ # which will resurrect the regions without an easy way to remove them
+ # (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:
+ view.erase_regions(msg.region_key)
+ view.erase_phantoms(msg.region_key)
+
+
+class PrimaryBatch(MessageBatch):
+
+ """A batch of messages with the primary message.
+
+ :ivar primary_message: The primary message object.
+ :ivar child_batches: List of `ChildBatch` batches associated with this
+ batch.
+ :ivar child_links: List of `(url, text)` tuples for links to child batches
+ that are "far away".
+ """
+
+ primary_message = None
+
+ def __init__(self, primary_message):
+ super(PrimaryBatch, self).__init__()
+ self.primary_message = primary_message
+ self.child_batches = []
+ self.child_links = []
+
+ def __iter__(self):
+ yield self.primary_message
+ for child in self.children:
+ yield child
+
+ def path(self):
+ return self.primary_message.path
+
+ def first(self):
+ return self.primary_message
+
+ def dismiss(self, window):
+ self.hidden = True
+ self._dismiss(window)
+ for batch in self.child_batches:
+ batch._dismiss(window)
+
+
+class ChildBatch(MessageBatch):
+
+ """A batch of messages that are associated with a primary message.
+
+ :ivar primary_batch: The `PrimaryBatch` this is associated with.
+ :ivar back_link: Tuple of `(url, text)` of the link to the primary batch
+ if it is "far away" (otherwise None).
+ """
+
+ primary_batch = None
+ back_link = None
+
+ def __init__(self, primary_batch):
+ super(ChildBatch, self).__init__()
+ self.primary_batch = primary_batch
+
+ def __iter__(self):
+ for child in self.children:
+ yield child
+
+ def path(self):
+ return self.children[0].path
+
+ def first(self):
+ return self.children[0]
+
+ def dismiss(self, window):
+ self.hidden = True
+ self.primary_batch.dismiss(window)
diff --git a/rust/messages.py b/rust/messages.py
index aa2e2163..1a5b928c 100644
--- a/rust/messages.py
+++ b/rust/messages.py
@@ -8,155 +8,173 @@
import itertools
import os
import re
+import urllib.parse
+import uuid
import webbrowser
-from . import util
+from . import util, themes
+from .batch import *
# Key is window id.
# Value is a dictionary: {
-# 'paths': {path: [msg_dict,...]},
-# 'msg_index': (path_idx, message_idx)
+# 'paths': {path: [MessageBatch, ...]},
+# 'batch_index': (path_idx, message_idx),
# }
+# `paths` is an OrderedDict to handle next/prev message.
# `path` is the absolute path to the file.
-# Each msg_dict has the following:
-# - `level`: Message level as a string such as "error", or "info".
-# - `span`: Location of the message (0-based):
-# `((line_start, col_start), (line_end, col_end))`
-# May be `None` to indicate no particular spot.
-# - `is_main`: If True, this is a top-level message. False is used for
-# attached detailed diagnostic information, child notes, etc.
-# - `path`: Absolute path to the file.
-# - `text`: The raw text of the message without any minihtml markup. May be
-# None if the content is raw markup.
-# - `minihtml_text`: The string used for showing phantoms that includes the
-# minihtml markup.
-# - `output_panel_region`: Optional Sublime Region object that indicates the
-# region in the build output panel that corresponds with this message.
WINDOW_MESSAGES = {}
LINK_PATTERN = r'(https?://[-a-zA-Z0-9@:%._+~#=]{2,256}\.[a-zA-Z]{2,6}\b[-a-zA-Z0-9@:%_+.~#?&/=]*)'
-CSS_TEMPLATE = """
-
-
-{content}
-
-"""
-
-POPUP_CSS = """
- body {
- margin: 0.25em;
- }
-"""
-
-
-def clear_messages(window):
- WINDOW_MESSAGES.pop(window.id(), None)
- for view in window.views():
- view.erase_phantoms('rust-syntax-phantom')
- view.erase_regions('rust-error')
- view.erase_regions('rust-warning')
- view.erase_regions('rust-note')
- view.erase_regions('rust-help')
+class Message:
+ """A diagnostic message.
-def add_message(window, path, span, level, is_main, text, minihtml_text, msg_cb):
- """Add a message to be displayed.
-
- :param window: The Sublime window.
- :param path: The absolute path of the file to show the message for.
- :param span: Location of the message (0-based):
+ :ivar id: A unique uuid for this message.
+ :ivar region_key: A string for the Sublime highlight region and phantom
+ for this message. Unique per view.
+ :ivar text: The raw text of the message without any minihtml markup. May
+ be None if the content is raw markup (such as a minihtml link) or if
+ it is an outline-only region (which happens with things such as
+ dual-region messages added in 1.21).
+ :ivar minihtml_text: The string used for showing phantoms that includes
+ the minihtml markup. May be None.
+ :ivar level: Message level as a string such as "error", or "info".
+ :ivar span: Location of the message (0-based):
`((line_start, col_start), (line_end, col_end))`
May be `None` to indicate no particular spot.
- :param level: The Rust message level ('error', 'note', etc.).
- :param is_main: If True, this is a top-level message. False is used for
- attached detailed diagnostic information, child notes, etc.
- :param text: The raw text of the message without any minihtml markup. May
- be None if there is no text (such as when adding pure markup).
- :param minihtml_text: The message to display with minihtml markup.
- :param msg_cb: Callback that will be given the message. May be None.
+ :ivar path: Absolute path to the file.
+ :ivar code: Rust error code as a string such as 'E0001'. May be None.
+ :ivar output_panel_region: Optional Sublime Region object that indicates
+ the region in the build output panel that corresponds with this
+ message.
+ :ivar primary: True if this is the primary message, False if a child.
+ :ivar children: List of additional Message objects. This is *not*
+ recursive (children cannot have children).
+ :ivar parent: The primary Message object if this a child.
"""
- if 'macros>' in path:
- # Macros from external crates will be displayed in the console
- # via msg_cb.
- return
- wid = window.id()
- try:
- messages_by_path = WINDOW_MESSAGES[wid]['paths']
- except KeyError:
- # This is an OrderedDict to handle next/prev message.
- messages_by_path = collections.OrderedDict()
- WINDOW_MESSAGES[wid] = {
- 'paths': messages_by_path,
- 'msg_index': (-1, -1)
- }
- messages = messages_by_path.setdefault(path, [])
-
- to_add = {
- 'path': path,
- 'level': level,
- 'span': span,
- 'is_main': is_main,
- 'text': text,
- 'minihtml_text': minihtml_text,
- }
- if _is_duplicate(to_add, messages):
- # Don't add duplicates.
- return
- messages.append(to_add)
- view = window.find_open_file(path)
- if view:
- _show_phantom(view, level, span, minihtml_text)
- if msg_cb:
- msg_cb(to_add)
-
-
-def _is_duplicate(to_add, messages):
- # Ignore 'minihtml_text' and 'output_panel_region' keys.
- keys = ('path', 'span', 'is_main', 'level', 'text')
- for message in messages:
+ region_key = None
+ text = None
+ minihtml_text = None
+ level = None
+ span = None
+ path = None
+ code = None
+ output_panel_region = None
+ primary = True
+ parent = None
+
+ def __init__(self):
+ self.id = uuid.uuid4()
+ self.children = []
+
+ def lineno(self, first=False):
+ """Return the line number of the message (0-based).
+
+ :param first: If True, returns the line number of the start of the
+ region. Otherwise returns the last line of the region.
+ """
+ if self.span:
+ if first:
+ return self.span[0][0]
+ else:
+ return self.span[1][0]
+ else:
+ return 999999999
+
+ def __iter__(self):
+ """Convenience iterator for iterating over the message and its children."""
+ yield self
+ for child in self.children:
+ yield child
+
+ def escaped_text(self, indent):
+ """Returns the minihtml markup of the message.
+
+ :param indent: String used for indentation when the message spans
+ multiple lines. Typically a series of to get correct
+ alignment.
+ """
+ if self.minihtml_text:
+ return self.minihtml_text
+ if not self.text:
+ return ''
+
+ def escape_and_link(i_txt):
+ i, txt = i_txt
+ if i % 2:
+ return '%s' % (txt, txt)
+ else:
+ # Call strip() because sometimes rust includes newlines at the
+ # end of the message, which we don't want.
+ escaped = html.escape(txt.strip(), quote=False)
+ return re.sub('^( +)', lambda m: ' '*len(m.group()), escaped, flags=re.MULTILINE)\
+ .replace('\n', '
' + indent)
+
+ parts = re.split(LINK_PATTERN, self.text)
+ return ' '.join(map(escape_and_link, enumerate(parts)))
+
+ def is_similar(self, other):
+ """Returns True if this message is essentially the same as the given
+ message. Used for deduplication."""
+ keys = ('path', 'span', 'level', 'text', 'minihtml_text')
for key in keys:
- if to_add[key] != message[key]:
- break
+ if getattr(other, key) != getattr(self, key):
+ return False
else:
return True
- return False
+
+ def sublime_region(self, view):
+ """Returns a sublime.Region object for this message."""
+ if self.span:
+ return sublime.Region(
+ view.text_point(self.span[0][0], self.span[0][1]),
+ view.text_point(self.span[1][0], self.span[1][1])
+ )
+ else:
+ # Place at bottom of file for lack of anywhere better.
+ return sublime.Region(view.size())
+
+ def __repr__(self):
+ result = ['')
+ return ''.join(result)
+
+
+def clear_messages(window):
+ """Remove all messages for the given window."""
+ for path, batches in WINDOW_MESSAGES.pop(window.id(), {})\
+ .get('paths', {})\
+ .items():
+ view = window.find_open_file(path)
+ if view:
+ for batch in batches:
+ for msg in batch:
+ view.erase_regions(msg.region_key)
+ view.erase_phantoms(msg.region_key)
+
+
+def clear_all_messages():
+ """Remove all messages in all windows."""
+ for window in sublime.windows():
+ if window.id() in WINDOW_MESSAGES:
+ clear_messages(window)
+
+
+def add_message(window, message):
+ """Add a message to be displayed (ignores children).
+
+ :param window: The Sublime window.
+ :param message: The `Message` object to add.
+ """
+ _save_batches(window, [PrimaryBatch(message)], None)
def has_message_for_path(window, path):
@@ -166,8 +184,8 @@ def has_message_for_path(window, path):
def messages_finished(window):
"""This should be called after all messages have been added."""
- _draw_all_region_highlights(window)
_sort_messages(window)
+ _draw_all_region_highlights(window)
def _draw_all_region_highlights(window):
@@ -175,119 +193,97 @@ def _draw_all_region_highlights(window):
been received since Sublime does not have an API to incrementally add
them."""
paths = WINDOW_MESSAGES.get(window.id(), {}).get('paths', {})
- for path, messages in paths.items():
+ for path, batches in paths.items():
view = window.find_open_file(path)
if view:
- _draw_region_highlights(view, messages)
+ _draw_region_highlights(view, batches)
-def _draw_region_highlights(view, messages):
- if util.get_setting('rust_region_style', 'outline') == 'none':
+def _draw_region_highlights(view, batches):
+ if util.get_setting('rust_region_style') == 'none':
return
+ # Collect message regions by level.
regions = {
'error': [],
'warning': [],
'note': [],
'help': [],
}
- for message in messages:
- region = _span_to_region(view, message['span'])
- if message['level'] not in regions:
- print('RustEnhanced: Unknown message level %r encountered.' % message['level'])
- message['level'] = 'error'
- regions[message['level']].append(region)
-
- # Remove lower-level regions that are identical to higher-level regions.
- def filter_out(to_filter, to_check):
- def check_in(region):
- for r in regions[to_check]:
- if r == region:
- return False
- return True
- regions[to_filter] = list(filter(check_in, regions[to_filter]))
- filter_out('help', 'note')
- filter_out('help', 'warning')
- filter_out('help', 'error')
- filter_out('note', 'warning')
- filter_out('note', 'error')
- filter_out('warning', 'error')
-
- package_name = __package__.split('.')[0]
- gutter_style = util.get_setting('rust_gutter_style', 'shape')
+ for batch in batches:
+ if batch.hidden:
+ continue
+ for msg in batch:
+ region = msg.sublime_region(view)
+ if msg.level not in regions:
+ print('RustEnhanced: Unknown message level %r encountered.' % msg.level)
+ msg.level = 'error'
+ regions[msg.level].append((msg.region_key, region))
# Do this in reverse order so that errors show on-top.
for level in ['help', 'note', 'warning', 'error']:
- # Unfortunately you cannot specify colors, but instead scopes as
- # defined in the color theme. If the scope is not defined, then it
- # will show up as foreground color (white in dark themes). I just use
- # "info" as an undefined scope (empty string will remove regions).
- # "invalid" will typically show up as red.
+ # Use scope names from color themes to drive the color of the outline.
+ # 'invalid' typically is red. We use 'info' for all other levels, which
+ # is usually not defined in any color theme, and will end up showing as
+ # the foreground color (white in dark themes).
+ #
+ # TODO: Consider using the new magic scope names added in build 3148
+ # to manually specify colors:
+ # region.redish, region.orangish, region.yellowish,
+ # region.greenish, region.bluish, region.purplish and
+ # region.pinkish
if level == 'error':
scope = 'invalid'
else:
scope = 'info'
- key = 'rust-%s' % level
- if gutter_style == 'none':
- icon = ''
- else:
- icon = 'Packages/%s/images/gutter/%s-%s.png' % (
- package_name, gutter_style, level)
- if regions[level]:
+ icon = util.icon_path(level)
+ for key, region in regions[level]:
_sublime_add_regions(
- view, key, regions[level], scope, icon,
+ view, key, [region], scope, icon,
sublime.DRAW_NO_FILL | sublime.DRAW_EMPTY)
-def _wrap_css(content, extra_css=''):
- """Takes the given minihtml content and places it inside a with the
- appropriate CSS."""
- return CSS_TEMPLATE.format(content=content,
- error_color=util.get_setting('rust_syntax_error_color', 'var(--redish)'),
- warning_color=util.get_setting('rust_syntax_warning_color', 'var(--yellowish)'),
- note_color=util.get_setting('rust_syntax_note_color', 'var(--greenish)'),
- help_color=util.get_setting('rust_syntax_help_color', 'var(--bluish)'),
- extra_css=extra_css,
- )
-
-
def message_popup(view, point, hover_zone):
"""Displays a popup if there is a message at the given point."""
paths = WINDOW_MESSAGES.get(view.window().id(), {}).get('paths', {})
- msgs = paths.get(view.file_name(), [])
+ batches = paths.get(view.file_name(), [])
if hover_zone == sublime.HOVER_GUTTER:
# Collect all messages on this line.
row = view.rowcol(point)[0]
- def filter_row(msg):
- span = msg['span']
+ def filter_row(batch):
+ span = batch.first().span
if span:
return row >= span[0][0] and row <= span[1][0]
else:
last_row = view.rowcol(view.size())[0]
return row == last_row
- msgs = filter(filter_row, msgs)
+ batches = filter(filter_row, batches)
else:
# Collect all messages covering this point.
- def filter_point(msg):
- span = msg['span']
- if span:
- start_pt = view.text_point(*span[0])
- end_pt = view.text_point(*span[1])
- return point >= start_pt and point <= end_pt
- else:
- return point == view.size()
-
- msgs = filter(filter_point, msgs)
-
- if msgs:
- to_show = '\n'.join(msg['minihtml_text'] for msg in msgs)
- minihtml = _wrap_css(to_show, extra_css=POPUP_CSS)
+ def filter_point(batch):
+ for msg in batch:
+ if msg.span:
+ start_pt = view.text_point(*msg.span[0])
+ end_pt = view.text_point(*msg.span[1])
+ if point >= start_pt and point <= end_pt:
+ return True
+ elif point == view.size():
+ return True
+ return False
+
+ batches = filter(filter_point, batches)
+
+ if batches:
+ theme = themes.THEMES[util.get_setting('rust_message_theme')]
+ minihtml = '\n'.join(theme.render(batch, for_popup=True) for batch in batches)
+ if not minihtml:
+ return
on_nav = functools.partial(_click_handler, view, hide_popup=True)
max_width = view.em_width() * 79
- view.show_popup(minihtml, sublime.COOPERATE_WITH_AUTO_COMPLETE,
+ _sublime_show_popup(view, minihtml, sublime.COOPERATE_WITH_AUTO_COMPLETE,
point, max_width=max_width, on_navigate=on_nav)
@@ -298,17 +294,48 @@ def _click_handler(view, url, hide_popup=False):
view.hide_popup()
elif url.startswith('file:///'):
view.window().open_file(url[8:], sublime.ENCODED_POSITION)
+ elif url.startswith('replace:'):
+ info = urllib.parse.parse_qs(url[8:])
+ _accept_replace(view, info['id'][0], info['replacement'][0])
+ if hide_popup:
+ view.hide_popup()
else:
webbrowser.open_new(url)
-def _show_phantom(view, level, span, minihtml_text):
- if util.get_setting('rust_phantom_style', 'normal') != 'normal':
+def _accept_replace(view, mid, replacement):
+ def batch_and_msg():
+ for batch in batches:
+ for msg in batch:
+ if str(msg.id) == mid:
+ return batch, msg
+ raise ValueError('Rust Enhanced internal error: Could not find ID %r' % (mid,))
+ batches = WINDOW_MESSAGES.get(view.window().id(), {})\
+ .get('paths', {})\
+ .get(view.file_name(), [])
+ batch, msg = batch_and_msg()
+ # Retrieve the updated region from Sublime (since it may have changed
+ # since the messages were generated).
+ regions = view.get_regions(msg.region_key)
+ if not regions:
+ print('Rust Enhanced internal error: Could not find region for suggestion.')
+ return
+ region = (regions[0].a, regions[0].b)
+ batch.dismiss(view.window())
+ view.run_command('rust_accept_suggested_replacement', {
+ 'region': region,
+ 'replacement': replacement
+ })
+
+
+def _show_phantom(view, batch):
+ if util.get_setting('rust_phantom_style') != 'normal':
return
- if not minihtml_text:
+ if batch.hidden:
return
- region = _span_to_region(view, span)
+ first = batch.first()
+ region = first.sublime_region(view)
# For some reason if you have a multi-line region, the phantom is only
# displayed under the first line. I think it makes more sense for the
# phantom to appear below the last line.
@@ -321,26 +348,20 @@ def _show_phantom(view, level, span, minihtml_text):
region.end()
)
+ theme = themes.THEMES[util.get_setting('rust_message_theme')]
+ content = theme.render(batch)
+ if not content:
+ return
+
_sublime_add_phantom(
view,
- 'rust-syntax-phantom', region,
- _wrap_css(minihtml_text),
+ first.region_key, region,
+ content,
sublime.LAYOUT_BLOCK,
functools.partial(_click_handler, view)
)
-def _span_to_region(view, span):
- if span:
- return sublime.Region(
- view.text_point(span[0][0], span[0][1]),
- view.text_point(span[1][0], span[1][1])
- )
- else:
- # Place at bottom of file for lack of anywhere better.
- return sublime.Region(view.size())
-
-
def _sublime_add_phantom(view, key, region, content, layout, on_navigate):
"""Pulled out to assist testing."""
view.add_phantom(
@@ -356,6 +377,11 @@ def _sublime_add_regions(view, key, regions, scope, icon, flags):
view.add_regions(key, regions, scope, icon, flags)
+def _sublime_show_popup(view, content, *args, **kwargs):
+ """Pulled out to assist testing."""
+ view.show_popup(content, *args, **kwargs)
+
+
def _sort_messages(window):
"""Sorts messages so that errors are shown first when using Next/Prev
commands."""
@@ -368,27 +394,23 @@ def _sort_messages(window):
window_info = WINDOW_MESSAGES[wid]
except KeyError:
return
- messages_by_path = window_info['paths']
+ batches_by_path = window_info['paths']
items = []
- for path, messages in messages_by_path.items():
- for message in messages:
+ for path, batches in batches_by_path.items():
+ for batch in batches:
level = {
'error': 0,
'warning': 1,
'note': 2,
'help': 3,
- }.get(message['level'], 0)
- if message['span']:
- lineno = message['span'][0][0]
- else:
- lineno = 99999999
- items.append((level, path, lineno, message))
+ }.get(batch.first().level, 0)
+ items.append((level, path, batch.first().lineno(), batch))
items.sort(key=lambda x: x[:3])
- messages_by_path = collections.OrderedDict()
- for _, path, _, message in items:
- messages = messages_by_path.setdefault(path, [])
- messages.append(message)
- window_info['paths'] = messages_by_path
+ batches_by_path = collections.OrderedDict()
+ for _, path, _, batch in items:
+ batches = batches_by_path.setdefault(path, [])
+ batches.append(batch)
+ window_info['paths'] = batches_by_path
def show_next_message(window, levels):
@@ -409,8 +431,9 @@ def _show_message(window, current_idx, transient=False, force_open=False):
except KeyError:
return
paths = window_info['paths']
- path, messages = _ith_iter_item(paths.items(), current_idx[0])
- msg = messages[current_idx[1]]
+ path, batches = _ith_iter_item(paths.items(), current_idx[0])
+ batch = batches[current_idx[1]]
+ msg = batch.first()
_scroll_build_panel(window, msg)
view = None
if not transient and not force_open:
@@ -426,23 +449,23 @@ def _show_message(window, current_idx, transient=False, force_open=False):
# focus. See:
# https://github.com/SublimeTextIssues/Core/issues/1041
flags |= sublime.TRANSIENT | sublime.FORCE_GROUP
- if msg['span']:
+ if msg.span:
# show_at_center is buggy with newly opened views (see
# https://github.com/SublimeTextIssues/Core/issues/538).
# ENCODED_POSITION is 1-based.
- row, col = msg['span'][0]
+ row, col = msg.span[0]
else:
row, col = (999999999, 1)
view = window.open_file('%s:%d:%d' % (path, row + 1, col + 1),
flags)
# Block until the view is loaded.
- _show_message_wait(view, messages, current_idx)
+ _show_message_wait(view)
-def _show_message_wait(view, messages, current_idx):
+def _show_message_wait(view):
if view.is_loading():
def f():
- _show_message_wait(view, messages, current_idx)
+ _show_message_wait(view)
sublime.set_timeout(f, 10)
# The on_load event handler will call show_messages_for_view which
# should handle displaying the messages.
@@ -451,16 +474,17 @@ def f():
def _scroll_build_panel(window, message):
"""If the build output panel is open, scroll the output to the message
selected."""
- if 'output_panel_region' in message:
+ if message.output_panel_region:
# Defer cyclic import.
from . import opanel
view = window.find_output_panel(opanel.PANEL_NAME)
if view:
view.sel().clear()
- region = message['output_panel_region']
+ region = message.output_panel_region
view.sel().add(region)
view.show(region)
# Force panel to update.
+ # TODO: See note about workaround below.
view.add_regions('bug', [region], 'bug', 'dot', sublime.HIDDEN)
view.erase_regions('bug')
@@ -469,13 +493,12 @@ def _scroll_to_message(view, message, transient):
"""Scroll view to the message."""
if not transient:
view.window().focus_view(view)
- r = _span_to_region(view, message['span'])
+ r = message.sublime_region(view)
view.sel().clear()
view.sel().add(r.a)
view.show_at_center(r)
- # Work around bug in Sublime where the visual of the cursor
- # does not update. See
- # https://github.com/SublimeTextIssues/Core/issues/485
+ # TODO: Fix this to use a TextCommand to properly handle undo.
+ # See https://github.com/SublimeTextIssues/Core/issues/485
view.add_regions('bug', [r], 'bug', 'dot', sublime.HIDDEN)
view.erase_regions('bug')
@@ -483,19 +506,12 @@ def _scroll_to_message(view, message, transient):
def show_messages_for_view(view):
"""Adds all phantoms and region outlines for a view."""
window = view.window()
- paths = WINDOW_MESSAGES.get(window.id(), {}).get('paths', {})
- messages = paths.get(view.file_name(), None)
- if messages:
- _show_messages_for_view(view, messages)
-
-
-def _show_messages_for_view(view, messages):
- for message in messages:
- _show_phantom(view,
- message['level'],
- message['span'],
- message['minihtml_text'])
- _draw_region_highlights(view, messages)
+ batches = WINDOW_MESSAGES.get(window.id(), {})\
+ .get('paths', {})\
+ .get(view.file_name(), [])
+ for batch in batches:
+ _show_phantom(view, batch)
+ _draw_region_highlights(view, batches)
def _ith_iter_item(d, i):
@@ -503,37 +519,37 @@ def _ith_iter_item(d, i):
def _advance_next_message(window, levels, wrap_around=False):
- """Update global msg_index to the next index."""
+ """Update global batch_index to the next index."""
try:
win_info = WINDOW_MESSAGES[window.id()]
except KeyError:
return None
paths = win_info['paths']
- path_idx, msg_idx = win_info['msg_index']
+ path_idx, batch_idx = win_info['batch_index']
if path_idx == -1:
# First time.
path_idx = 0
- msg_idx = 0
+ batch_idx = 0
else:
- msg_idx += 1
+ batch_idx += 1
while path_idx < len(paths):
- messages = _ith_iter_item(paths.values(), path_idx)
- while msg_idx < len(messages):
- msg = messages[msg_idx]
- if _is_matching_level(levels, msg):
- current_idx = (path_idx, msg_idx)
- win_info['msg_index'] = current_idx
+ batches = _ith_iter_item(paths.values(), path_idx)
+ while batch_idx < len(batches):
+ batch = batches[batch_idx]
+ if _is_matching_level(levels, batch.first()):
+ current_idx = (path_idx, batch_idx)
+ win_info['batch_index'] = current_idx
return current_idx
- msg_idx += 1
+ batch_idx += 1
path_idx += 1
- msg_idx = 0
+ batch_idx = 0
if wrap_around:
# No matching entries, give up.
return None
else:
# Start over at the beginning of the list.
- win_info['msg_index'] = (-1, -1)
+ win_info['batch_index'] = (-1, -1)
return _advance_next_message(window, levels, wrap_around=True)
@@ -544,50 +560,49 @@ def _last_index(paths):
def _advance_prev_message(window, levels, wrap_around=False):
- """Update global msg_index to the previous index."""
+ """Update global batch_index to the previous index."""
try:
win_info = WINDOW_MESSAGES[window.id()]
except KeyError:
return None
paths = win_info['paths']
- path_idx, msg_idx = win_info['msg_index']
+ path_idx, batch_idx = win_info['batch_index']
if path_idx == -1:
# First time, start at the end.
- path_idx, msg_idx = _last_index(paths)
+ path_idx, batch_idx = _last_index(paths)
else:
- msg_idx -= 1
+ batch_idx -= 1
while path_idx >= 0:
- messages = _ith_iter_item(paths.values(), path_idx)
- while msg_idx >= 0:
- msg = messages[msg_idx]
- if _is_matching_level(levels, msg):
- current_idx = (path_idx, msg_idx)
- win_info['msg_index'] = current_idx
+ batches = _ith_iter_item(paths.values(), path_idx)
+ while batch_idx >= 0:
+ batch = batches[batch_idx]
+ if _is_matching_level(levels, batch.first()):
+ current_idx = (path_idx, batch_idx)
+ win_info['batch_index'] = current_idx
return current_idx
- msg_idx -= 1
+ batch_idx -= 1
path_idx -= 1
if path_idx >= 0:
- msg_idx = len(_ith_iter_item(paths.values(), path_idx)) - 1
+ batch_idx = len(_ith_iter_item(paths.values(), path_idx)) - 1
if wrap_around:
# No matching entries, give up.
return None
else:
# Start over at the end of the list.
- win_info['msg_index'] = (-1, -1)
+ win_info['batch_index'] = (-1, -1)
return _advance_prev_message(window, levels, wrap_around=True)
-def _is_matching_level(levels, msg_dict):
- if not msg_dict['is_main']:
+def _is_matching_level(levels, message):
+ if not message.primary:
# Only navigate to top-level messages.
return False
- level = msg_dict['level']
if levels == 'all':
return True
- elif levels == 'error' and level == 'error':
+ elif levels == 'error' and message.level == 'error':
return True
- elif levels == 'warning' and level != 'error':
+ elif levels == 'warning' and message.level != 'error':
# Warning, Note, Help
return True
else:
@@ -613,18 +628,19 @@ def list_messages(window):
return
panel_items = []
jump_to = []
- for path_idx, (path, msgs) in enumerate(win_info['paths'].items()):
- for msg_idx, msg_dict in enumerate(msgs):
- if not msg_dict['is_main']:
+ for path_idx, (path, batches) in enumerate(win_info['paths'].items()):
+ for batch_idx, batch in enumerate(batches):
+ if not isinstance(batch, PrimaryBatch):
continue
- jump_to.append((path_idx, msg_idx))
- if msg_dict['span']:
+ message = batch.primary_message
+ jump_to.append((path_idx, batch_idx))
+ if message.span:
path_label = '%s:%s' % (
_relative_path(window, path),
- msg_dict['span'][0][0] + 1)
+ message.span[0][0] + 1)
else:
path_label = _relative_path(window, path)
- item = [msg_dict['text'], path_label]
+ item = [message.text, path_label]
panel_items.append(item)
def on_done(idx):
@@ -639,21 +655,13 @@ def on_highlighted(idx):
def add_rust_messages(window, base_path, info, target_path, msg_cb):
"""Add messages from Rust JSON to Sublime views.
- - `window`: Sublime Window object.
- - `base_path`: Base path used for resolving relative paths from Rust.
- - `info`: Dictionary of messages from rustc or cargo.
- - `target_path`: Absolute path to the top-level source file of the target
- (lib.rs, main.rs, etc.). May be None if it is not known.
- - `msg_cb`: Function called for each message (if not None). It is given a
- single parameter, a dictionary of the message to display with the
- following keys:
- - `path`: Full path to the file. None if no file associated.
- - `span`: Sublime (0-based) offsets into the file for the region
- `((line_start, col_start), (line_end, col_end))`. None if no
- region.
- - `level`: Rust level ('error', 'warning', 'note', etc.)
- - `is_main`: If True, a top-level message.
- - `text`: Raw text of the message without markup.
+ :param window: Sublime Window object.
+ :param base_path: Base path used for resolving relative paths from Rust.
+ :param info: Dictionary of messages from rustc or cargo.
+ :param target_path: Absolute path to the top-level source file of the
+ target (lib.rs, main.rs, etc.). May be None if it is not known.
+ :param msg_cb: Callback that will be given the message object (and each
+ child separately).
"""
# cargo check emits in a slightly different format.
if 'reason' in info:
@@ -664,95 +672,32 @@ def add_rust_messages(window, base_path, info, target_path, msg_cb):
# 'compiler-artifact' or 'build-script-executed'.
return
- # Each message dictionary contains the following:
- # - 'text': The text of the message.
- # - 'level': The level (a string such as 'error').
- # - 'span_path': Absolute path to the file for this message.
- # - 'span_region': Sublime region where the message is. Tuple of
- # ((line_start, column_start), (line_end, column_end)), 0-based. None
- # if no region.
- # - 'is_main': Boolean of whether or not this is the main message. Only
- # the `main_message` should be True.
- # - 'help_link': Optional string of an HTML link for additional
- # information on the message.
- # - 'links': Optional string of HTML code that contains links to other
- # messages (populated by _create_cross_links). Should only be set in
- # `main_message`.
- # - 'back_link': Optional string of HTML code that is a link back to the
- # main message (populated by _create_cross_links).
- main_message = {}
- # List of message dictionaries, belonging to the main message.
- additional_messages = []
+ primary_message = Message()
_collect_rust_messages(window, base_path, info, target_path, msg_cb, {},
- main_message, additional_messages)
-
- messages = _create_cross_links(main_message, additional_messages)
-
- content_template = '{level}{msg}{help_link}{back_link}
\xD7 '
- links_template = '{indent}{links}
'
-
- last_level = None
- last_path = None
- for message in messages:
- level = message['level']
- cls = {
- 'error': 'rust-error',
- 'warning': 'rust-warning',
- 'note': 'rust-note',
- 'help': 'rust-help',
- }.get(level, 'rust-error')
- indent = ' ' * (len(level) + 2)
- if level == last_level and message['span_path'] == last_path:
- level_text = indent
- else:
- level_text = '%s: ' % (level,)
- last_level = level
- last_path = message['span_path']
-
- def escape_and_link(i_txt):
- i, txt = i_txt
- if i % 2:
- return '%s' % (txt, txt)
- else:
- # Call strip() because sometimes rust includes newlines at the
- # end of the message, which we don't want.
- return html.escape(txt.strip(), quote=False).\
- replace('\n', '
' + indent)
-
- if message['text']:
- parts = re.split(LINK_PATTERN, message['text'])
- escaped_text = ''.join(map(escape_and_link, enumerate(parts)))
-
- content = content_template.format(
- cls=cls,
- level=level_text,
- msg=escaped_text,
- help_link=message.get('help_link', ''),
- back_link=message.get('back_link', ''),
- )
- else:
- content = None
-
- add_message(window, message['span_path'], message['span_region'],
- level, message['is_main'], message['text'], content,
- msg_cb)
-
- if main_message.get('links'):
- content = links_template.format(
- indent=' ' * (len(main_message['level']) + 2),
- links=main_message['links']
- )
- add_message(window,
- main_message['span_path'],
- main_message['span_region'],
- main_message['level'],
- False, None, content, None)
+ primary_message)
+ if not primary_message.path:
+ return
+ if _is_duplicate_message(window, primary_message):
+ return
+ batches = _batch_and_cross_link(primary_message)
+ _save_batches(window, batches, msg_cb)
+
+
+def _is_duplicate_message(window, primary_message):
+ batches = WINDOW_MESSAGES.get(window.id(), {})\
+ .get('paths', {})\
+ .get(primary_message.path, [])
+ for batch in batches:
+ if isinstance(batch, PrimaryBatch):
+ if batch.primary_message.is_similar(primary_message):
+ return True
+ return False
def _collect_rust_messages(window, base_path, info, target_path,
msg_cb, parent_info,
- main_message, additional_messages):
+ message):
"""
- `info`: The dictionary from Rust has the following structure:
@@ -792,9 +737,7 @@ def _collect_rust_messages(window, base_path, info, target_path,
None (AFAIK, this only happens when is_primary is True, in which
case the main 'message' is all that should be displayed).
- 'suggested_replacement': If not None, a string with a
- suggestion of the code to replace this span. If this is set, we
- actually display the 'rendered' value instead, because it's
- easier to read.
+ suggestion of the code to replace this span.
- 'expansion': If not None, a dictionary indicating the expansion
of the macro within this span. The values are:
@@ -819,13 +762,11 @@ def _collect_rust_messages(window, base_path, info, target_path,
- `parent_info`: Dictionary used for tracking "children" messages.
Currently only has 'span' key, the span of the parent to display the
message (for children without spans).
- - `main_message`: Dictionary where we store the main message information.
- - `additional_messages`: List where we add dictionaries of messages that
- are associated with the main message.
+ - `message`: `Message` object where we store the message information.
"""
# Include "notes" tied to errors, even if warnings are disabled.
if (info['level'] != 'error' and
- util.get_setting('rust_syntax_hide_warnings', False) and
+ util.get_setting('rust_syntax_hide_warnings') and
not parent_info
):
return
@@ -842,31 +783,34 @@ def make_span_region(span):
else:
return None
- def set_primary_message(span, message):
+ def set_primary_message(span, text):
parent_info['span'] = span
# Not all codes have explanations (yet).
if info['code'] and info['code']['explanation']:
- # TODO
- # This could potentially be a link that opens a Sublime popup, or
- # a new temp buffer with the contents of 'explanation'.
- # (maybe use sublime-markdown-popups)
- main_message['help_link'] = \
- ' ?' % (
- info['code']['code'],)
- main_message['span_path'] = make_span_path(span)
- main_message['span_region'] = make_span_region(span)
- main_message['text'] = message
- main_message['level'] = info['level']
- main_message['is_main'] = True
+ message.code = info['code']['code']
+ message.path = make_span_path(span)
+ message.span = make_span_region(span)
+ message.text = text
+ message.level = info['level']
def add_additional(span, text, level):
- additional_messages.append({
- 'span_path': make_span_path(span),
- 'span_region': make_span_region(span),
- 'text': text,
- 'level': level,
- 'is_main': False,
- })
+ child = Message()
+ child.text = text
+ child.level = level
+ child.primary = False
+ if 'macros>' in span['file_name']:
+ # Nowhere to display this, just send it to the console via msg_cb.
+ msg_cb(child)
+ else:
+ child.path = make_span_path(span)
+ child.span = make_span_region(span)
+ if any(map(lambda m: m.is_similar(child), message.children)):
+ # Duplicate message, skip. This happens with some of the
+ # macro help messages.
+ return child
+ child.parent = message
+ message.children.append(child)
+ return child
if len(info['spans']) == 0:
if parent_info:
@@ -879,7 +823,9 @@ def add_additional(span, text, level):
# Some of the messages are not very interesting, though.
imsg = info['message']
if not (imsg.startswith('aborting due to') or
- imsg.startswith('cannot continue')):
+ imsg.startswith('cannot continue') or
+ imsg.startswith('Some errors occurred') or
+ imsg.startswith('For more information about')):
if target_path:
# Display at the bottom of the root path (like main.rs)
# for lack of a better place to put it.
@@ -889,13 +835,10 @@ def add_additional(span, text, level):
# Not displayed as a phantom since we don't know where to
# put it.
if msg_cb:
- msg_cb({
- 'path': None,
- 'level': info['level'],
- 'span': None,
- 'is_main': True,
- 'text': imsg,
- })
+ tmp_msg = Message()
+ tmp_msg.level = info['level']
+ tmp_msg.text = imsg
+ msg_cb(tmp_msg)
def find_span_r(span, expansion=None):
if span['expansion']:
@@ -958,7 +901,7 @@ def find_span_r(span, expansion=None):
# Check if the main message is already set since there might
# be multiple spans that are primary (in which case, we
# arbitrarily show the main message on the first one).
- if not main_message:
+ if not message.path:
set_primary_message(span, info['message'])
label = span['label']
@@ -970,89 +913,121 @@ def find_span_r(span, expansion=None):
#
# Label with an empty string can happen for messages that have
# multiple spans (starting in 1.21).
- if label != None:
+ if label is not None:
# Display the label for this Span.
add_additional(span, label, info['level'])
if span['suggested_replacement']:
# The "suggested_replacement" contains the code that
- # should replace the span. However, it can be easier to
- # read if you repeat the entire line (from "rendered").
- add_additional(span, span['suggested_replacement'], 'help')
+ # should replace the span.
+ child = add_additional(span, None, 'help')
+ replacement_template = util.multiline_fix("""
+
+ """)
+ child.minihtml_text = replacement_template % (
+ urllib.parse.urlencode({
+ 'id': child.id,
+ 'replacement': span['suggested_replacement'],
+ }),
+ html.escape(span['suggested_replacement'], quote=False),
+ )
# Recurse into children (which typically hold notes).
for child in info['children']:
_collect_rust_messages(window, base_path, child, target_path,
msg_cb, parent_info.copy(),
- main_message, additional_messages)
-
+ message)
-def _create_cross_links(main_message, additional_messages):
- """Returns a list of dictionaries of messages to be displayed.
-
- This is responsible for creating links from the main message to any
- additional messages.
- """
- if not main_message:
- return []
+def _batch_and_cross_link(primary_message):
+ """Creates a list of MessageBatch objects with appropriate cross links."""
def make_file_path(msg):
- if msg['span_region']:
+ if msg.span:
return 'file:///%s:%s:%s' % (
- msg['span_path'].replace('\\', '/'),
- msg['span_region'][0][0] + 1,
- msg['span_region'][0][1] + 1,
+ msg.path.replace('\\', '/'),
+ msg.span[1][0] + 1,
+ msg.span[1][1] + 1,
)
else:
# Arbitrarily large line number to force it to the bottom of the
# file, since we don't know ahead of time how large the file is.
- return 'file:///%s:999999999' % (msg['span_path'],)
-
- back_link = '\u2190' % (make_file_path(main_message),)
-
- def get_lineno(msg):
- if msg['span_region']:
- return msg['span_region'][0][0]
- else:
- return 999999999
-
- link_set = set()
- links = []
- link_template = 'Note: {filename}{lineno}'
- for msg in additional_messages:
- msg_lineno = get_lineno(msg)
- seen_key = (msg['span_path'], msg_lineno)
- # Only include a link if it is not close to the main message.
- if msg['span_path'] != main_message['span_path'] or \
- abs(msg_lineno - get_lineno(main_message)) > 5:
- if seen_key in link_set:
- continue
- link_set.add(seen_key)
- if msg['span_region']:
- lineno = ':%s' % (msg_lineno + 1,)
- else:
- # AFAIK, this code path is not possible, but leaving it here
- # to be safe.
- lineno = ''
- if msg['span_path'] == main_message['span_path']:
- if get_lineno(msg) < get_lineno(main_message):
- filename = '\u2191' # up arrow
- else:
- filename = '\u2193' # down arrow
+ return 'file:///%s:999999999' % (msg.path,)
+
+ # Group messages by line.
+ primary_batch = PrimaryBatch(primary_message)
+ path_line_map = collections.OrderedDict()
+ key = (primary_message.path, primary_message.lineno())
+ path_line_map[key] = primary_batch
+ for msg in primary_message.children:
+ key = (msg.path, msg.lineno())
+ try:
+ batch = path_line_map[key]
+ except KeyError:
+ batch = ChildBatch(primary_batch)
+ primary_batch.child_batches.append(batch)
+ path_line_map[key] = batch
+ batch.children.append(msg)
+
+ def make_link_text(msg, other):
+ # text for msg -> other
+ if msg.path == other.path:
+ if msg.lineno() < other.lineno():
+ filename = '\u2193' # down arrow
else:
- filename = os.path.basename(msg['span_path'])
- links.append(link_template.format(
- url=make_file_path(msg),
- filename=filename,
- lineno=lineno,
- ))
- msg['back_link'] = back_link
-
- if links:
- link_text = '\n'.join(links)
- else:
- link_text = ''
- main_message['links'] = link_text
+ filename = '\u2191' # up arrow
+ else:
+ filename = os.path.basename(other.path)
+ if other.span:
+ return '%s:%s' % (filename, other.lineno() + 1,)
+ else:
+ return filename
+
+ # Create cross links.
+ back_url = make_file_path(primary_message)
+
+ for (path, lineno), batch in path_line_map.items():
+ if batch == primary_batch:
+ continue
+ # Only include a link if the message is "far away".
+ msg = batch.first()
+ if msg.path != primary_message.path or \
+ abs(msg.lineno() - primary_message.lineno()) > 5:
+ url = make_file_path(msg)
+ text = make_link_text(primary_message, msg)
+ primary_batch.child_links.append((url, text))
+ back_text = make_link_text(msg, primary_message)
+ batch.back_link = (back_url, back_text)
+
+ return list(path_line_map.values())
+
+
+def _save_batches(window, batches, msg_cb):
+ """Save the batches. This does several things:
+
+ - Saves batches to WINDOW_MESSAGES global.
+ - Updates the region_key for each message.
+ - Displays phantoms if a view is already open.
+ - Calls `msg_cb` for each individual message.
+ """
+ wid = window.id()
+ try:
+ path_to_batches = WINDOW_MESSAGES[wid]['paths']
+ except KeyError:
+ path_to_batches = collections.OrderedDict()
+ WINDOW_MESSAGES[wid] = {
+ 'paths': path_to_batches,
+ 'batch_index': (-1, -1)
+ }
- result = additional_messages[:]
- result.insert(0, main_message)
- return result
+ for batch in batches:
+ path_batches = path_to_batches.setdefault(batch.path(), [])
+ # Flatten to a list of messages so each message gets a unique ID.
+ num = len(list(itertools.chain.from_iterable(path_batches)))
+ 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)
+ if msg_cb:
+ for msg in batch:
+ msg_cb(msg)
diff --git a/rust/opanel.py b/rust/opanel.py
index ee9316f8..01bd4952 100644
--- a/rust/opanel.py
+++ b/rust/opanel.py
@@ -85,24 +85,21 @@ def on_data(self, proc, data):
# crate's source file (such as libcore), which is probably
# not available.
return
+ message = messages.Message()
lineno = int(m.group(2)) - 1
# Region columns appear to the left, so this is +1.
col = int(m.group(3))
# Rust 1.24 changed column numbering to be 1-based.
if semver.match(self.rustc_version, '>=1.24.0-beta'):
col -= 1
- span = ((lineno, col), (lineno, col))
+ message.span = ((lineno, col), (lineno, col))
# +2 to skip ", "
build_region = sublime.Region(region_start + m.start() + 2,
region_start + m.end())
-
- # Use callback so the build output window scrolls to this
- # point.
- def on_test_cb(message):
- message['output_panel_region'] = build_region
-
- messages.add_message(self.window, path, span, 'error', True,
- None, None, on_test_cb)
+ message.output_panel_region = build_region
+ message.path = path
+ message.level = 'error'
+ messages.add_message(self.window, message)
def on_error(self, proc, message):
self._append(message)
@@ -113,23 +110,30 @@ def on_json(self, proc, obj):
None, self.msg_cb)
def msg_cb(self, message):
- level = message['level']
- region_start = self.output_view.size() + len(level) + 2
- path = message['path']
+ """Display the message in the output panel. Also marks the message
+ with the output panel region where the message is shown. This allows
+ us to scroll the output panel to the correct region when cycling
+ through messages.
+ """
+ if not message.text:
+ # Region-only messages can be ignored.
+ return
+ region_start = self.output_view.size() + len(message.level) + 2
+ path = message.path
if path:
if self.base_path and path.startswith(self.base_path):
path = os.path.relpath(path, self.base_path)
- if message['span']:
- highlight_text = '%s:%d' % (path, message['span'][0][0] + 1)
+ if message.span:
+ highlight_text = '%s:%d' % (path, message.span[0][0] + 1)
else:
highlight_text = path
- self._append('%s: %s: %s' % (level, highlight_text, message['text']))
+ self._append('%s: %s: %s' % (message.level, highlight_text, message.text))
region = sublime.Region(region_start,
region_start + len(highlight_text))
else:
- self._append('%s: %s' % (level, message['text']))
+ self._append('%s: %s' % (message.level, message.text))
region = sublime.Region(region_start)
- message['output_panel_region'] = region
+ message.output_panel_region = region
def on_finished(self, proc, rc):
if rc:
diff --git a/rust/themes.py b/rust/themes.py
new file mode 100644
index 00000000..941e9fcb
--- /dev/null
+++ b/rust/themes.py
@@ -0,0 +1,338 @@
+"""Themes for different message styles."""
+
+from . import util
+from .batch import *
+
+
+POPUP_CSS = 'body { margin: 0.25em; }'
+
+
+def _help_link(code):
+ if code:
+ return ' ?' % (
+ code,)
+ else:
+ return ''
+
+
+class Theme:
+
+ """Base class for themes."""
+
+ def render(self, batch, for_popup=False):
+ """Return a minihtml string of the content in the message batch."""
+ raise NotImplementedError()
+
+
+class ClearTheme(Theme):
+
+ """Theme with a clear background, and colors matching the user's color
+ scheme."""
+
+ TMPL = util.multiline_fix("""
+
+
+ {content}
+
+ """)
+
+ MSG_TMPL = util.multiline_fix("""
+
+ {level_text}{text}{help_link}{close_link}
+
+ """)
+
+ LINK_TMPL = util.multiline_fix("""
+
+ """)
+
+ def render(self, batch, for_popup=False):
+ if for_popup:
+ extra_css = POPUP_CSS
+ else:
+ extra_css = ''
+
+ # Collect all the messages for this batch.
+ msgs = []
+ last_level = None
+ for i, msg in enumerate(batch):
+ text = msg.escaped_text('')
+ if not text:
+ continue
+ if msg.minihtml_text:
+ level_text = ''
+ else:
+ if msg.level == last_level:
+ level_text = ' ' * (len(msg.level) + 2)
+ else:
+ level_text = '%s: ' % (msg.level,)
+ last_level = msg.level
+ if i == 0:
+ # Only show close link on first message of a batch.
+ close_link = ' \xD7'
+ else:
+ close_link = ''
+ msgs.append(self.MSG_TMPL.format(
+ level=msg.level,
+ level_text=level_text,
+ text=text,
+ help_link=_help_link(msg.code),
+ close_link=close_link,
+ ))
+
+ # Add cross-links.
+ if isinstance(batch, PrimaryBatch):
+ for url, path in batch.child_links:
+ msgs.append(self.LINK_TMPL.format(
+ url=url, text='See Also:', path=path))
+ else:
+ if batch.back_link:
+ msgs.append(self.LINK_TMPL.format(
+ url=batch.back_link[0],
+ text='See Primary:',
+ path=batch.back_link[1]))
+
+ return self.TMPL.format(
+ error_color=util.get_setting('rust_syntax_error_color'),
+ warning_color=util.get_setting('rust_syntax_warning_color'),
+ note_color=util.get_setting('rust_syntax_note_color'),
+ help_color=util.get_setting('rust_syntax_help_color'),
+ content=''.join(msgs),
+ extra_css=extra_css)
+
+
+class SolidTheme(Theme):
+
+ """Theme with a solid background color."""
+
+ TMPL = util.multiline_fix("""
+
+
+ {content}
+
+ """)
+
+ PRIMARY_MSG_TMPL = util.multiline_fix("""
+
+ {icon} {text}{help_link}
\xD7
+ {children}
+ {links}
+
+ """)
+
+ SECONDARY_MSG_TMPL = util.multiline_fix("""
+
+ {children}
+ {links}
+
+ """)
+
+ CHILD_TMPL = util.multiline_fix("""
+ {icon} {text}
+ """)
+
+ LINK_TMPL = util.multiline_fix("""
+
+ """)
+
+ def render(self, batch, for_popup=False):
+
+ def icon(level):
+ # minihtml does not support switching resolution for images based on DPI.
+ # Always use the @2x images, and downscale on 1x displays. It doesn't
+ # look as good, but is close enough.
+ # See https://github.com/SublimeTextIssues/Core/issues/2228
+ path = util.icon_path(level, res=2)
+ if not path:
+ return ''
+ else:
+ return '
' % (path,)
+
+ if for_popup:
+ extra_css = POPUP_CSS
+ else:
+ extra_css = ''
+
+ # Collect all the child messages together.
+ children = []
+ for child in batch.children:
+ # Don't show the icon for children with the same level as the
+ # primary message.
+ if isinstance(batch, PrimaryBatch) and child.level == batch.primary_message.level:
+ child_icon = icon('none')
+ else:
+ child_icon = icon(child.level)
+ minihtml_text = child.escaped_text(' ' + icon('none'))
+ if minihtml_text:
+ txt = self.CHILD_TMPL.format(level=child.level,
+ icon=child_icon,
+ text=minihtml_text)
+ children.append(txt)
+
+ if isinstance(batch, PrimaryBatch):
+ links = []
+ for url, path in batch.child_links:
+ links.append(
+ self.LINK_TMPL.format(
+ url=url, text='See Also:', path=path))
+ text = batch.primary_message.escaped_text('')
+ if not text and not children:
+ return None
+ content = self.PRIMARY_MSG_TMPL.format(
+ level=batch.primary_message.level,
+ icon=icon(batch.primary_message.level),
+ text=text,
+ help_link=_help_link(batch.primary_message.code),
+ children=''.join(children),
+ links=''.join(links))
+ else:
+ if batch.back_link:
+ link = self.LINK_TMPL.format(url=batch.back_link[0],
+ text='See Primary:',
+ path=batch.back_link[1])
+ else:
+ link = ''
+ content = self.SECONDARY_MSG_TMPL.format(
+ level=batch.primary_batch.primary_message.level,
+ icon=icon(batch.primary_batch.primary_message.level),
+ children=''.join(children),
+ links=link)
+
+ return self.TMPL.format(content=content, extra_css=extra_css)
+
+
+class TestTheme(Theme):
+
+ """Theme used by tests for verifying which messages are displayed."""
+
+ def __init__(self):
+ self.path_messages = {}
+
+ def render(self, batch, for_popup=False):
+ from .messages import Message
+ messages = self.path_messages.setdefault(batch.first().path, [])
+ for msg in batch:
+ # Region-only messages will get checked by the region-checking
+ # code.
+ if msg.text or msg.minihtml_text:
+ messages.append(msg)
+
+ # Create fake messages for the links to simplify the test code.
+ def add_fake(msg, text):
+ fake = Message()
+ fake.text = text
+ fake.span = msg.span
+ fake.path = msg.path
+ fake.level = ''
+ messages.append(fake)
+
+ if isinstance(batch, PrimaryBatch):
+ for link in batch.child_links:
+ add_fake(batch.primary_message, 'See Also: ' + link[1])
+ else:
+ if batch.back_link:
+ add_fake(batch.first(), 'See Primary: ' + batch.back_link[1])
+ return None
+
+
+THEMES = {
+ 'clear': ClearTheme(),
+ 'solid': SolidTheme(),
+ 'test': TestTheme(),
+}
diff --git a/rust/util.py b/rust/util.py
index 07d638e9..d2e522f3 100644
--- a/rust/util.py
+++ b/rust/util.py
@@ -145,3 +145,20 @@ def get_cargo_metadata(window, cwd, toolchain=None):
return output[0]
else:
return None
+
+
+def icon_path(level, res=None):
+ """Return a path to a message-level icon."""
+ 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:
+ if res:
+ res_suffix = '@%ix' % (res,)
+ else:
+ res_suffix = ''
+ return 'Packages/%s/images/gutter/%s-%s%s.png' % (
+ package_name, gutter_style, level, res_suffix)
diff --git a/tests/error-tests/examples/SNAKE.rs b/tests/error-tests/examples/SNAKE.rs
index 3ca75716..ec11287b 100644
--- a/tests/error-tests/examples/SNAKE.rs
+++ b/tests/error-tests/examples/SNAKE.rs
@@ -1,4 +1,4 @@
fn main() {
}
// end-msg: WARN crate `SNAKE` should have a snake case
-// end-msg: NOTE #[warn(non_snake_case)] on by default
+// end-msg: NOTE(>=1.17.0) #[warn(non_snake_case)] on by default
diff --git a/tests/error-tests/examples/no_main.rs b/tests/error-tests/examples/no_main.rs
index f8a2c586..e3195933 100644
--- a/tests/error-tests/examples/no_main.rs
+++ b/tests/error-tests/examples/no_main.rs
@@ -4,6 +4,6 @@ mod no_main_mod;
// Not sure why no-trans doesn't handle this properly.
// When --profile=test is used with `cargo check`, this error will not happen
// due to the synthesized main created by the test harness.
-// end-msg: ERR(rust_syntax_checking_include_tests=False) main function not found
-// end-msg: NOTE(rust_syntax_checking_include_tests=False) the main function must be defined
-// end-msg: MSG(rust_syntax_checking_include_tests=False) Note: no_main_mod.rs:1
+// end-msg: ERR(rust_syntax_checking_include_tests=False OR <1.23.0,rust_syntax_checking_include_tests=True,check) /`?main`? function not found/
+// end-msg: NOTE(rust_syntax_checking_include_tests=False OR <1.23.0,rust_syntax_checking_include_tests=True,check) the main function must be defined
+// end-msg: MSG(rust_syntax_checking_include_tests=False OR <1.23.0,rust_syntax_checking_include_tests=True,check) See Also: no_main_mod.rs:4
diff --git a/tests/error-tests/examples/no_main_mod.rs b/tests/error-tests/examples/no_main_mod.rs
index 4d1c0224..f84f355d 100644
--- a/tests/error-tests/examples/no_main_mod.rs
+++ b/tests/error-tests/examples/no_main_mod.rs
@@ -1,7 +1,8 @@
/*BEGIN*/fn main() {
-// ^^^^^^^^^WARN(rust_syntax_checking_include_tests=True) function is never used
-// ^^^^^^^^^NOTE(rust_syntax_checking_include_tests=True) #[warn(dead_code)]
-// 1.24 nightly has changed how these no-trans messages are displayed (instead
-// of encompassing the entire function).
+// ^^^^^^^^^WARN(>=1.23.0,rust_syntax_checking_include_tests=True) function is never used
+// ^^^^^^^^^NOTE(>=1.23.0,rust_syntax_checking_include_tests=True) #[warn(dead_code)]
}/*END*/
-// ~NOTE(rust_syntax_checking_include_tests=False) here is a function named 'main'
+// ~NOTE(rust_syntax_checking_include_tests=False OR <1.23.0,rust_syntax_checking_include_tests=True,check) here is a function named 'main'
+// ~MSG(rust_syntax_checking_include_tests=False OR <1.23.0,rust_syntax_checking_include_tests=True,check) See Primary: no_main.rs
+// ~WARN(<1.19.0,no-trans,rust_syntax_checking_include_tests=True) function is never used
+// ~NOTE(>=1.17.0,<1.19.0,no-trans,rust_syntax_checking_include_tests=True) #[warn(dead_code)]
diff --git a/tests/error-tests/src/lib.rs b/tests/error-tests/src/lib.rs
index 4d89d2ad..0fd0ebcc 100644
--- a/tests/error-tests/src/lib.rs
+++ b/tests/error-tests/src/lib.rs
@@ -1,19 +1,19 @@
#[cfg(test)]
mod tests {
fn bad(a: DoesNotExist) {
-// ^^^^^^^^^^^^ERR(<1.16.0) undefined or not in scope
-// ^^^^^^^^^^^^ERR(<1.16.0) type name
-// ^^^^^^^^^^^^HELP(<1.16.0) no candidates
-// ^^^^^^^^^^^^ERR(>=1.16.0,rust_syntax_checking_include_tests=True) not found in this scope
-// ^^^^^^^^^^^^ERR(>=1.16.0,rust_syntax_checking_include_tests=True) cannot find type `DoesNotExist`
+// ^^^^^^^^^^^^ERR(<1.16.0,rust_syntax_checking_include_tests=True) type name
+// ^^^^^^^^^^^^ERR(<1.16.0,rust_syntax_checking_include_tests=True) undefined or not in scope
+// ^^^^^^^^^^^^HELP(<1.16.0,rust_syntax_checking_include_tests=True) no candidates
+// ^^^^^^^^^^^^ERR(>=1.16.0,<1.19.0,rust_syntax_checking_include_tests=True,no-trans OR >=1.23.0,rust_syntax_checking_include_tests=True) cannot find type `DoesNotExist`
+// ^^^^^^^^^^^^ERR(>=1.16.0,<1.19.0,rust_syntax_checking_include_tests=True,no-trans OR >=1.23.0,rust_syntax_checking_include_tests=True) not found in this scope
}
#[test]
fn it_works() {
asdf
-// ^^^^ERR(<1.16.0) unresolved name
-// ^^^^ERR(<1.16.0) unresolved name
-// ^^^^ERR(>=1.16.0,rust_syntax_checking_include_tests=True) not found in this scope
-// ^^^^ERR(>=1.16.0,rust_syntax_checking_include_tests=True) cannot find value
+// ^^^^ERR(<1.16.0,rust_syntax_checking_include_tests=True) unresolved name
+// ^^^^ERR(<1.16.0,rust_syntax_checking_include_tests=True) unresolved name
+// ^^^^ERR(>=1.16.0,<1.19.0,rust_syntax_checking_include_tests=True,no-trans OR >=1.23.0,rust_syntax_checking_include_tests=True) cannot find value
+// ^^^^ERR(>=1.16.0,<1.19.0,rust_syntax_checking_include_tests=True,no-trans OR >=1.23.0,rust_syntax_checking_include_tests=True) not found in this scope
}
}
diff --git a/tests/error-tests/tests/arg-count-mismatch.rs b/tests/error-tests/tests/arg-count-mismatch.rs
index 88c5c0ee..ebe1a53c 100644
--- a/tests/error-tests/tests/arg-count-mismatch.rs
+++ b/tests/error-tests/tests/arg-count-mismatch.rs
@@ -12,12 +12,15 @@
/*BEGIN*/fn f(x: isize) {
// ^^^^^^^^^^^^^^ERR(>=1.24.0-beta) defined here
+// ^^^^^^^^^^^^^^MSG(>=1.24.0-beta) See Primary: ↓:22
}/*END*/
// ~ERR(<1.24.0-beta) defined here
+// ~MSG(<1.24.0-beta) See Primary: ↓:22
// children without spans, spans with no labels
// Should display error (with link) and a note of expected type.
fn main() { let i: (); i = f(); }
// ^^^ERR this function takes 1 parameter
// ^^^ERR expected 1 parameter
-// ^^^MSG Note: ↑:13
+// ^^^MSG(<1.24.0-beta) See Also: ↑:16
+// ^^^MSG(>=1.24.0-beta) See Also: ↑:13
diff --git a/tests/error-tests/tests/cast-to-unsized-trait-object-suggestion.rs b/tests/error-tests/tests/cast-to-unsized-trait-object-suggestion.rs
index 2cb137aa..cea962ef 100644
--- a/tests/error-tests/tests/cast-to-unsized-trait-object-suggestion.rs
+++ b/tests/error-tests/tests/cast-to-unsized-trait-object-suggestion.rs
@@ -12,9 +12,9 @@ fn main() {
&1 as Send;
// ^^^^^^^^^^ERR cast to unsized type
// ^^^^HELP try casting to
-// ^^^^HELP &Send
+// ^^^^HELP /Accept Replacement:.*&Send/
Box::new(1) as Send;
// ^^^^^^^^^^^^^^^^^^^ERR cast to unsized type
// ^^^^HELP try casting to a `Box` instead
-// ^^^^HELP Box
+// ^^^^HELP /Accept Replacement:.*Box/
}
diff --git a/tests/error-tests/tests/error_across_mod.rs b/tests/error-tests/tests/error_across_mod.rs
index 935a1301..15e3b07c 100644
--- a/tests/error-tests/tests/error_across_mod.rs
+++ b/tests/error-tests/tests/error_across_mod.rs
@@ -4,8 +4,8 @@ fn test() {
error_across_mod_f::f(1);
// ^ERR(<1.24.0-beta) this function takes 0 parameters but 1
// ^ERR(<1.24.0-beta) expected 0 parameters
-// ^MSG(<1.24.0-beta) Note: error_across_mod_f.rs:1
+// ^MSG(<1.24.0-beta) See Also: error_across_mod_f.rs:4
// ^^^^^^^^^^^^^^^^^^^^^^^^ERR(>=1.24.0-beta) this function takes 0 parameters but 1
// ^^^^^^^^^^^^^^^^^^^^^^^^ERR(>=1.24.0-beta) expected 0 parameters
-// ^^^^^^^^^^^^^^^^^^^^^^^^MSG(>=1.24.0-beta) Note: error_across_mod_f.rs:1
+// ^^^^^^^^^^^^^^^^^^^^^^^^MSG(>=1.24.0-beta) See Also: error_across_mod_f.rs:1
}
diff --git a/tests/error-tests/tests/error_across_mod_f.rs b/tests/error-tests/tests/error_across_mod_f.rs
index f4d6bea2..f6ce1d73 100644
--- a/tests/error-tests/tests/error_across_mod_f.rs
+++ b/tests/error-tests/tests/error_across_mod_f.rs
@@ -1,4 +1,6 @@
/*BEGIN*/pub fn f() {
// ^^^^^^^^^^ERR(>=1.24.0-beta) defined here
+// ^^^^^^^^^^MSG(>=1.24.0-beta) See Primary: error_across_mod.rs:4
}/*END*/
// ~ERR(<1.24.0-beta) defined here
+// ~MSG(<1.24.0-beta) See Primary: error_across_mod.rs:4
diff --git a/tests/error-tests/tests/macro-backtrace-println.rs b/tests/error-tests/tests/macro-backtrace-println.rs
index b72dae9e..ac71a139 100644
--- a/tests/error-tests/tests/macro-backtrace-println.rs
+++ b/tests/error-tests/tests/macro-backtrace-println.rs
@@ -24,10 +24,11 @@ macro_rules! myprintln {
($fmt:expr) => (myprint!(concat!($fmt, "\n")));
// ^^^^^^^^^^^^^^^^^^^ERR(<1.23.0-beta) invalid reference
// ^^^^^^^^^^^^^^^^^^^ERR(>=1.23.0-beta) 1 positional argument
-// ^^^^^^^^^^^^^^^^^^^MSG Note: ↓:31
+// ^^^^^^^^^^^^^^^^^^^MSG See Also: ↓:31
}
fn main() {
myprintln!("{}");
// ^^^^^^^^^^^^^^^^^HELP in this macro invocation
+// ^^^^^^^^^^^^^^^^^MSG See Primary: ↑:24
}
diff --git a/tests/error-tests/tests/macro-expansion-inside-1.rs b/tests/error-tests/tests/macro-expansion-inside-1.rs
index 0d1dbb14..920ac23b 100644
--- a/tests/error-tests/tests/macro-expansion-inside-1.rs
+++ b/tests/error-tests/tests/macro-expansion-inside-1.rs
@@ -4,4 +4,7 @@ mod macro_expansion_inside_mod1;
// This is an example of an error in a macro from another module.
/*BEGIN*/example_bad_syntax!{}/*END*/
-// ~HELP(>=1.20.0-beta) in this macro invocation
+// ~HELP(>=1.20.0) in this macro invocation
+// ~HELP(>=1.20.0) in this macro invocation
+// ~MSG(>=1.20.0) See Primary: macro_expansion_inside_mod1.rs:7
+// ~MSG(>=1.20.0) See Primary: macro_expansion_inside_mod1.rs:7
diff --git a/tests/error-tests/tests/macro-expansion-inside-2.rs b/tests/error-tests/tests/macro-expansion-inside-2.rs
index d36b91cf..d98e75da 100644
--- a/tests/error-tests/tests/macro-expansion-inside-2.rs
+++ b/tests/error-tests/tests/macro-expansion-inside-2.rs
@@ -6,4 +6,5 @@ mod macro_expansion_inside_mod2;
fn f() {
let x: () = example_bad_value!();
// ^^^^^^^^^^^^^^^^^^^^HELP in this macro invocation
+// ^^^^^^^^^^^^^^^^^^^^MSG See Primary: macro_expansion_inside_mod2.rs:3
}
diff --git a/tests/error-tests/tests/macro-expansion-outside-1.rs b/tests/error-tests/tests/macro-expansion-outside-1.rs
index 4e138959..5254f8c8 100644
--- a/tests/error-tests/tests/macro-expansion-outside-1.rs
+++ b/tests/error-tests/tests/macro-expansion-outside-1.rs
@@ -12,18 +12,26 @@ extern crate dcrate;
// ~ERR(>=1.20.0) /expected one of .* here/
// ~ERR(>=1.20.0,<1.24.0-beta) unexpected token
// ~ERR(>=1.20.0) /expected one of .*, found `:`/
+// ~ERR(>=1.20.0) this error originates in a macro outside of the current crate
// ~ERR(>=1.20.0) expected one of
+// ~ERR(>=1.20.0,<1.24.0-beta) unexpected token
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) /expected one of .*, found `:`/
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) Errors occurred in macro from external crate
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) Macro text: ( ) => { enum E { Kind ( x : u32 ) } }
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) /expected one of .* here/
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) unexpected token
-// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) expected one of 7 possible tokens here
// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) /expected one of .*, found `:`/
+// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) Errors occurred in macro from external crate
+// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) Macro text: ( ) => { enum E { Kind ( x : u32 ) } }
+// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) expected one of 7 possible tokens here
+// end-msg: ERR(check,>=1.19.0,<1.20.0-beta) unexpected token
// end-msg: ERR(<1.19.0) /expected one of .*, found `:`/
// end-msg: ERR(<1.19.0) Errors occurred in macro from external crate
// end-msg: ERR(<1.19.0) Macro text: ( ) => { enum E { Kind ( x : u32 ) } }
// end-msg: ERR(>=1.18.0,<1.19.0) /expected one of .* here/
// end-msg: ERR(>=1.18.0,<1.19.0) unexpected token
// end-msg: ERR(<1.19.0) /expected one of .*, found `:`/
+// end-msg: ERR(<1.19.0) Errors occurred in macro from external crate
+// end-msg: ERR(<1.19.0) Macro text: ( ) => { enum E { Kind ( x : u32 ) } }
// end-msg: ERR(>=1.18.0,<1.19.0) expected one of 7 possible tokens here
+// end-msg: ERR(>=1.18.0,<1.19.0) unexpected token
diff --git a/tests/error-tests/tests/macro-expansion-outside-2.rs b/tests/error-tests/tests/macro-expansion-outside-2.rs
index cf4ce32b..d8f17028 100644
--- a/tests/error-tests/tests/macro-expansion-outside-2.rs
+++ b/tests/error-tests/tests/macro-expansion-outside-2.rs
@@ -11,4 +11,5 @@ fn f() {
// ^^^^^^^^^^^^^^^^^^^^ERR this error originates in a macro outside
// ^^^^^^^^^^^^^^^^^^^^ERR expected (), found i32
// ^^^^^^^^^^^^^^^^^^^^NOTE expected type
+// ^^^^^^^^^^^^^^^^^^^^NOTE(<1.16.0) found type
}
diff --git a/tests/error-tests/tests/macro_expansion_inside_mod1.rs b/tests/error-tests/tests/macro_expansion_inside_mod1.rs
index 6b4d3ea2..cacbb6c9 100644
--- a/tests/error-tests/tests/macro_expansion_inside_mod1.rs
+++ b/tests/error-tests/tests/macro_expansion_inside_mod1.rs
@@ -2,12 +2,15 @@
macro_rules! example_bad_syntax {
() => {
enum E {
+ // This is somewhat of an odd example, since rustc gives two
+ // syntax errors.
Kind(x: u32)
// ^ERR /expected one of .*, found `:`/
// ^ERR(>=1.18.0) /expected one of .* here/
-// ^MSG(>=1.20.0) Note: macro-expansion-inside-1.rs:6
+// ^MSG(>=1.20.0) See Also: macro-expansion-inside-1.rs:6
// ^ERR /expected one of .*, found `:`/
// ^ERR(>=1.18.0) expected one of
+// ^MSG(>=1.20.0) See Also: macro-expansion-inside-1.rs:6
}
}
}
diff --git a/tests/error-tests/tests/macro_expansion_inside_mod2.rs b/tests/error-tests/tests/macro_expansion_inside_mod2.rs
index 78f78424..6a616a15 100644
--- a/tests/error-tests/tests/macro_expansion_inside_mod2.rs
+++ b/tests/error-tests/tests/macro_expansion_inside_mod2.rs
@@ -4,5 +4,6 @@ macro_rules! example_bad_value {
// ^^^^ERR mismatched types
// ^^^^ERR expected (), found i32
// ^^^^NOTE expected type `()`
-// ^^^^MSG macro-expansion-inside-2.rs:7
+// ^^^^NOTE(<1.16.0) found type `i32`
+// ^^^^MSG See Also: macro-expansion-inside-2.rs:7
}
diff --git a/tests/error-tests/tests/method-ambig-two-traits-with-default-method.rs b/tests/error-tests/tests/method-ambig-two-traits-with-default-method.rs
new file mode 100644
index 00000000..5d0545d4
--- /dev/null
+++ b/tests/error-tests/tests/method-ambig-two-traits-with-default-method.rs
@@ -0,0 +1,33 @@
+// Copyright 2013 The Rust Project Developers. See the COPYRIGHT
+// file at the top-level directory of this distribution and at
+// http://rust-lang.org/COPYRIGHT.
+//
+// Licensed under the Apache License, Version 2.0 or the MIT license
+// , at your
+// option. This file may not be copied, modified, or distributed
+// except according to those terms.
+
+// This test exercises a message with multiple "far away" child messages.
+
+trait Foo { fn method(&self) {} }
+// ^^^^^^^^^^^^^^^^^^^NOTE(<1.24.0) candidate #1
+// ^^^^^^^^^^^^^^^^^^^MSG(<1.24.0) See Primary: ↓:28
+// ^^^^^^^^^^^^^^^^NOTE(>=1.24.0) candidate #1
+// ^^^^^^^^^^^^^^^^MSG(>=1.24.0) See Primary: ↓:28
+trait Bar { fn method(&self) {} }
+// ^^^^^^^^^^^^^^^^^^^NOTE(<1.24.0) candidate #2
+// ^^^^^^^^^^^^^^^^^^^MSG(<1.24.0) See Primary: ↓:28
+// ^^^^^^^^^^^^^^^^NOTE(>=1.24.0) candidate #2
+// ^^^^^^^^^^^^^^^^MSG(>=1.24.0) See Primary: ↓:28
+
+impl Foo for usize {}
+impl Bar for usize {}
+
+fn main() {
+ 1_usize.method();
+// ^^^^^^ERR multiple applicable items
+// ^^^^^^ERR multiple `method` found
+// ^^^^^^MSG See Also: ↑:13
+// ^^^^^^MSG See Also: ↑:18
+}
diff --git a/tests/error-tests/tests/remote_note_1.rs b/tests/error-tests/tests/remote_note_1.rs
index aa232b30..ebd9c743 100644
--- a/tests/error-tests/tests/remote_note_1.rs
+++ b/tests/error-tests/tests/remote_note_1.rs
@@ -1,4 +1,5 @@
#![deny(unreachable_code)]
// ^^^^^^^^^^^^^^^^NOTE lint level defined here
+// ^^^^^^^^^^^^^^^^MSG See Primary: remote_note_1_mod.rs:3
pub mod remote_note_1_mod;
diff --git a/tests/error-tests/tests/remote_note_1_mod.rs b/tests/error-tests/tests/remote_note_1_mod.rs
index 25cd77f6..6ae95450 100644
--- a/tests/error-tests/tests/remote_note_1_mod.rs
+++ b/tests/error-tests/tests/remote_note_1_mod.rs
@@ -3,5 +3,5 @@ pub fn f() {
println!("Paul is dead");
// ^^^^^^^^^^^^^^^^^^^^^^^^^ERR unreachable statement
// ^^^^^^^^^^^^^^^^^^^^^^^^^ERR this error originates in a macro outside of the current crate
-// ^^^^^^^^^^^^^^^^^^^^^^^^^MSG Note: remote_note_1.rs:1
+// ^^^^^^^^^^^^^^^^^^^^^^^^^MSG See Also: remote_note_1.rs:1
}
diff --git a/tests/error-tests/tests/test_new_lifetime_message.rs b/tests/error-tests/tests/test_new_lifetime_message.rs
index aeecd3a5..05d4c7f0 100644
--- a/tests/error-tests/tests/test_new_lifetime_message.rs
+++ b/tests/error-tests/tests/test_new_lifetime_message.rs
@@ -9,3 +9,4 @@
}/*END*/
// ~NOTE(<1.21.0) ...the reference is valid
// ~NOTE(<1.21.0) ...but the borrowed content
+// ~HELP(<1.16.0) consider using an explicit lifetime
diff --git a/tests/error-tests/tests/test_unicode.rs b/tests/error-tests/tests/test_unicode.rs
index 80533f04..8ac7e8e2 100644
--- a/tests/error-tests/tests/test_unicode.rs
+++ b/tests/error-tests/tests/test_unicode.rs
@@ -7,5 +7,5 @@ fn main() {
// ^^^NOTE(>=1.21.0,<1.22.0) to disable this warning
// ^^^NOTE(>=1.22.0,<1.25.0-beta) to avoid this warning
// ^^^HELP(>=1.25.0-beta) consider using `_foo` instead
-// ^^^HELP(>=1.25.0-beta) _foo
+// ^^^HELP(>=1.25.0-beta) /Accept Replacement:.*_foo/
}
diff --git a/tests/multi-targets/tests/common/helpers.rs b/tests/multi-targets/tests/common/helpers.rs
index ff2610e5..9447248e 100644
--- a/tests/multi-targets/tests/common/helpers.rs
+++ b/tests/multi-targets/tests/common/helpers.rs
@@ -9,7 +9,7 @@
/*BEGIN*/pub fn unused() {
// ^^^^^^^^^^^^^^^WARN(>=1.22.0) function is never used
-// ^^^^^^^^^^^^^^^NOTE(<1.24.0-beta) #[warn(dead_code)]
+// ^^^^^^^^^^^^^^^NOTE(>=1.22.0,<1.24.0-beta) #[warn(dead_code)]
}/*END*/
// ~WARN(<1.22.0) function is never used
// ~NOTE(<1.22.0,>=1.17.0) #[warn(dead_code)]
diff --git a/tests/rust_test_common.py b/tests/rust_test_common.py
index bf3cda58..0c721d4b 100644
--- a/tests/rust_test_common.py
+++ b/tests/rust_test_common.py
@@ -23,6 +23,7 @@
cargo_config = plugin.rust.cargo_config
target_detect = plugin.rust.target_detect
messages = plugin.rust.messages
+themes = plugin.rust.themes
util = plugin.rust.util
semver = plugin.rust.semver
@@ -72,6 +73,11 @@ def setUp(self):
self.settings = sublime.load_settings('RustEnhanced.sublime-settings')
self._override_setting('show_panel_on_build', False)
self._override_setting('cargo_build', {})
+ # Disable incremental compilation (first enabled in 1.24). It slows
+ # down the tests.
+ self._override_setting('rust_env', {
+ 'CARGO_INCREMENTAL': '0',
+ })
# Clear any state.
messages.clear_messages(window)
@@ -79,6 +85,8 @@ def setUp(self):
window.create_output_panel(plugin.rust.opanel.PANEL_NAME)
def _override_setting(self, name, value):
+ """Tests can call this to override a Sublime setting, which will get
+ restored once the test is complete."""
if name not in self._original_settings:
if self.settings.has(name):
self._original_settings[name] = self.settings.get(name)
@@ -171,6 +179,8 @@ def async_test_view():
finally:
if view.window():
window.focus_view(view)
+ if view.is_dirty():
+ view.run_command('revert')
window.run_command('close_file')
def _cargo_clean(self, view_or_path):
@@ -215,3 +225,64 @@ def __exit__(self, type, value, traceback):
def __str__(self):
return '%s=%s' % (self.name, self.value)
+
+
+class UiIntercept(object):
+
+ """Context manager that assists with mocking some Sublime UI components."""
+
+ def __init__(self, passthrough=False):
+ self.passthrough = passthrough
+
+ def __enter__(self):
+ self.phantoms = {}
+ self.view_regions = {}
+ self.popups = {}
+
+ def collect_popups(v, content, flags=0, location=-1,
+ max_width=None, max_height=None,
+ on_navigate=None, on_hide=None):
+ ps = self.popups.setdefault(v.file_name(), [])
+ result = {'view': v,
+ 'content': content,
+ 'flags': flags,
+ 'location': location,
+ 'max_width': max_width,
+ 'max_height': max_height,
+ 'on_navigate': on_navigate,
+ 'on_hide': on_hide}
+ ps.append(result)
+ if self.passthrough:
+ filtered = {k: v for (k, v) in result.items() if v is not None}
+ self.orig_show_popup(**filtered)
+
+ def collect_phantoms(v, key, region, content, layout, on_navigate):
+ ps = self.phantoms.setdefault(v.file_name(), [])
+ ps.append({
+ 'region': region,
+ 'content': content,
+ 'on_navigate': on_navigate,
+ })
+ if self.passthrough:
+ self.orig_add_phantom(v, key, region, content, layout, on_navigate)
+
+ def collect_regions(v, key, regions, scope, icon, flags):
+ rs = self.view_regions.setdefault(v.file_name(), [])
+ rs.extend(regions)
+ if self.passthrough:
+ self.orig_add_regions(v, key, regions, scope, icon, flags)
+
+ m = plugin.rust.messages
+ self.orig_add_phantom = m._sublime_add_phantom
+ self.orig_add_regions = m._sublime_add_regions
+ self.orig_show_popup = m._sublime_show_popup
+ m._sublime_add_phantom = collect_phantoms
+ m._sublime_add_regions = collect_regions
+ m._sublime_show_popup = collect_popups
+ return self
+
+ def __exit__(self, type, value, traceback):
+ m = plugin.rust.messages
+ m._sublime_add_phantom = self.orig_add_phantom
+ m._sublime_add_regions = self.orig_add_regions
+ m._sublime_show_popup = self.orig_show_popup
diff --git a/tests/test_cargo_build.py b/tests/test_cargo_build.py
index db30bf31..d3dbf5c0 100644
--- a/tests/test_cargo_build.py
+++ b/tests/test_cargo_build.py
@@ -341,11 +341,11 @@ def _test_clippy(self, view):
self._check_added_message(window, view.file_name(), r'char_lit_as_u8')
def _check_added_message(self, window, filename, pattern):
- msgs = messages.WINDOW_MESSAGES[window.id()]
- path_msgs = msgs['paths'][filename]
- for msg in path_msgs:
- if re.search(pattern, msg['text']):
- break
+ batches = messages.WINDOW_MESSAGES[window.id()]['paths'][filename]
+ for batch in batches:
+ for msg in batch:
+ if re.search(pattern, msg.text):
+ return
else:
raise AssertionError('Failed to find %r' % pattern)
diff --git a/tests/test_click_handler.py b/tests/test_click_handler.py
new file mode 100644
index 00000000..bbb6aa3f
--- /dev/null
+++ b/tests/test_click_handler.py
@@ -0,0 +1,37 @@
+"""Tests for clicking on messages."""
+
+import re
+
+from rust_test_common import *
+
+
+class TestClickHandler(TestBase):
+
+ def test_accept_replacement(self):
+ self._with_open_file('tests/error-tests/tests/cast-to-unsized-trait-object-suggestion.rs',
+ self._test_accept_replacement)
+
+ def _test_accept_replacement(self, view):
+ def get_line(lineno):
+ pt = view.text_point(lineno, 0)
+ line_r = view.line(pt)
+ return view.substr(line_r)
+
+ with UiIntercept(passthrough=True) as ui:
+ e = plugin.SyntaxCheckPlugin.RustSyntaxCheckEvent()
+ self._cargo_clean(view)
+ e.on_post_save(view)
+ self._get_rust_thread().join()
+ self.assertEqual(get_line(11), ' &1 as Send;')
+ self.assertEqual(get_line(15), ' Box::new(1) as Send;')
+ phantoms = ui.phantoms[view.file_name()]
+ phantoms.sort(key=lambda x: x['region'])
+ # Filter out just the "Accept Replacement" phantoms.
+ phantoms = filter(lambda x: 'Replacement' in x['content'], phantoms)
+ expected = ((11, ' &1 as &Send;'),
+ (15, ' Box::new(1) as Box;'))
+ for phantom, (lineno, expected_line) in zip(phantoms, expected):
+ url = re.search(r'=1.16.0) not found in this scope
// ^^^^ERR(>=1.16.0) cannot find function
}
diff --git a/tests/workspace/workspace2/src/somemod.rs b/tests/workspace/workspace2/src/somemod.rs
index 7e6bd949..8d6630ac 100644
--- a/tests/workspace/workspace2/src/somemod.rs
+++ b/tests/workspace/workspace2/src/somemod.rs
@@ -1,7 +1,7 @@
fn f() {
someerr
-// ^^^^^^^ERR(<1.16.0) unresolved name
// ^^^^^^^ERR(<1.16.0) unresolved name `someerr`
+// ^^^^^^^ERR(<1.16.0) unresolved name
// ^^^^^^^ERR(>=1.16.0) not found in this scope
// ^^^^^^^ERR(>=1.16.0) cannot find value
}