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. ![testingrust](https://cloud.githubusercontent.com/assets/43198/22944409/7780ab9a-f2a5-11e6-87ea-0e253d6c40f6.png) @@ -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 = '' - - 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(""" +
Accept Replacement: %s
+ """) + 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 }