Skip to content

Commit f47507e

Browse files
ehussjasonwilliams
authored andcommitted
Support messages with multiple regions and multiple suggestions. (#301)
This adds better support for messages that have multiple regions or have multiple suggestions. It also fixes suggestions that are empty strings (a suggestion that only removes code).
1 parent ccfc27a commit f47507e

File tree

7 files changed

+226
-72
lines changed

7 files changed

+226
-72
lines changed

rust/batch.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ def first(self):
2626
"""Returns the first message of the batch."""
2727
raise NotImplementedError()
2828

29-
def dismiss(self):
29+
def primary(self):
30+
"""Return the primary batch."""
31+
raise NotImplementedError()
32+
33+
def dismiss(self, window):
3034
"""Permanently remove this message and all its children from the
3135
view."""
3236
raise NotImplementedError()
@@ -80,6 +84,9 @@ def path(self):
8084
def first(self):
8185
return self.primary_message
8286

87+
def primary(self):
88+
return self
89+
8390
def dismiss(self, window):
8491
self.hidden = True
8592
self._dismiss(window)
@@ -113,6 +120,9 @@ def path(self):
113120
def first(self):
114121
return self.children[0]
115122

123+
def primary(self):
124+
return self.primary_batch
125+
116126
def dismiss(self, window):
117127
self.hidden = True
118-
self.primary_batch.dismiss(window)
128+
self._dismiss(window)

rust/messages.py

+87-53
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ class Message:
4242
be None if the content is raw markup (such as a minihtml link) or if
4343
it is an outline-only region (which happens with things such as
4444
dual-region messages added in 1.21).
45-
:ivar minihtml_text: The string used for showing phantoms that includes
46-
the minihtml markup. May be None.
4745
:ivar level: Message level as a string such as "error", or "info".
4846
:ivar span: Location of the message (0-based):
4947
`((line_start, col_start), (line_end, col_end))`
@@ -57,17 +55,21 @@ class Message:
5755
:ivar children: List of additional Message objects. This is *not*
5856
recursive (children cannot have children).
5957
:ivar parent: The primary Message object if this a child.
58+
:iver hidden: If true, don't show this message.
59+
:ivar suggested_replacement: An optional string of text as a suggestion to
60+
replace at the given span. If this is set, `text` will NOT be set.
6061
"""
6162
region_key = None
6263
text = None
63-
minihtml_text = None
6464
level = None
6565
span = None
6666
path = None
6767
code = None
6868
output_panel_region = None
6969
primary = True
7070
parent = None
71+
hidden = False
72+
suggested_replacement = None
7173

7274
def __init__(self):
7375
self.id = uuid.uuid4()
@@ -100,8 +102,8 @@ def escaped_text(self, view, indent):
100102
multiple lines. Typically a series of   to get correct
101103
alignment.
102104
"""
103-
if self.minihtml_text:
104-
return self.minihtml_text
105+
if self.suggested_replacement is not None:
106+
return self._render_suggested_replacement()
105107
if not self.text:
106108
return ''
107109

@@ -127,10 +129,43 @@ def escape_and_link(i_txt):
127129
parts = re.split(LINK_PATTERN, text)
128130
return ' '.join(map(escape_and_link, enumerate(parts)))
129131

132+
def _render_suggested_replacement(self):
133+
replacement_template = util.multiline_fix("""
134+
<div class="rust-replacement"><a href="replace:%s" class="rust-button">Accept Replacement:</a> %s</div>
135+
""")
136+
html_suggestion = html.escape(self.suggested_replacement, quote=False)
137+
if '\n' in html_suggestion:
138+
# Start on a new line so the text doesn't look too weird.
139+
html_suggestion = '\n' + html_suggestion
140+
html_suggestion = html_suggestion\
141+
.replace(' ', '&nbsp;')\
142+
.replace('\n', '<br>\n')
143+
return replacement_template % (
144+
urllib.parse.urlencode({
145+
'id': self.id,
146+
'replacement': self.suggested_replacement,
147+
}),
148+
html_suggestion,
149+
)
150+
151+
def suggestion_count(self):
152+
"""Number of suggestions in this message.
153+
154+
This is used to know once all suggestions have been accepted that a
155+
message can be dismissed.
156+
"""
157+
if self.parent:
158+
return self.parent.suggestion_count()
159+
count = 0
160+
for m in self:
161+
if m.suggested_replacement is not None and not m.hidden:
162+
count += 1
163+
return count
164+
130165
def is_similar(self, other):
131166
"""Returns True if this message is essentially the same as the given
132167
message. Used for deduplication."""
133-
keys = ('path', 'span', 'level', 'text', 'minihtml_text')
168+
keys = ('path', 'span', 'level', 'text', 'suggested_replacement')
134169
for key in keys:
135170
if getattr(other, key) != getattr(self, key):
136171
return False
@@ -140,10 +175,18 @@ def is_similar(self, other):
140175
def sublime_region(self, view):
141176
"""Returns a sublime.Region object for this message."""
142177
if self.span:
143-
return sublime.Region(
144-
view.text_point(self.span[0][0], self.span[0][1]),
145-
view.text_point(self.span[1][0], self.span[1][1])
146-
)
178+
regions = view.get_regions(self.region_key)
179+
if regions:
180+
self.span = (
181+
view.rowcol(regions[0].a),
182+
view.rowcol(regions[0].b)
183+
)
184+
return regions[0]
185+
else:
186+
return sublime.Region(
187+
view.text_point(self.span[0][0], self.span[0][1]),
188+
view.text_point(self.span[1][0], self.span[1][1])
189+
)
147190
else:
148191
# Place at bottom of file for lack of anywhere better.
149192
return sublime.Region(view.size())
@@ -265,24 +308,21 @@ def message_popup(view, point, hover_zone):
265308
row = view.rowcol(point)[0]
266309

267310
def filter_row(batch):
268-
span = batch.first().span
269-
if span:
270-
return row >= span[0][0] and row <= span[1][0]
271-
else:
272-
last_row = view.rowcol(view.size())[0]
273-
return row == last_row
311+
if batch.hidden:
312+
return False
313+
region = batch.first().sublime_region(view)
314+
batch_row_a = view.rowcol(region.begin())[0]
315+
batch_row_b = view.rowcol(region.end())[0]
316+
return row >= batch_row_a and row <= batch_row_b
274317

275318
batches = filter(filter_row, batches)
276319
else:
277320
# Collect all messages covering this point.
278321
def filter_point(batch):
322+
if batch.hidden:
323+
return False
279324
for msg in batch:
280-
if msg.span:
281-
start_pt = view.text_point(*msg.span[0])
282-
end_pt = view.text_point(*msg.span[1])
283-
if point >= start_pt and point <= end_pt:
284-
return True
285-
elif point == view.size():
325+
if not msg.hidden and msg.sublime_region(view).contains(point):
286326
return True
287327
return False
288328

@@ -307,7 +347,7 @@ def _click_handler(view, url, hide_popup=False):
307347
elif url.startswith('file:///'):
308348
view.window().open_file(url[8:], sublime.ENCODED_POSITION)
309349
elif url.startswith('replace:'):
310-
info = urllib.parse.parse_qs(url[8:])
350+
info = urllib.parse.parse_qs(url[8:], keep_blank_values=True)
311351
_accept_replace(view, info['id'][0], info['replacement'][0])
312352
if hide_popup:
313353
view.hide_popup()
@@ -333,11 +373,21 @@ def batch_and_msg():
333373
print('Rust Enhanced internal error: Could not find region for suggestion.')
334374
return
335375
region = (regions[0].a, regions[0].b)
336-
batch.dismiss(view.window())
337376
view.run_command('rust_accept_suggested_replacement', {
338377
'region': region,
339378
'replacement': replacement
340379
})
380+
msg.hidden = True
381+
if msg.suggestion_count():
382+
# Additional suggestions still exist, re-render the phantom.
383+
view.erase_phantoms(batch.first().region_key)
384+
for m in batch:
385+
# Force `span` to be updated to the most recent value.
386+
m.sublime_region(view)
387+
_show_phantom(view, batch)
388+
else:
389+
# No more suggestions, just hide the diagnostic completely.
390+
batch.primary().dismiss(view.window())
341391

342392

343393
def _show_phantom(view, batch):
@@ -820,24 +870,28 @@ def set_primary_message(span, text):
820870
message.text = text
821871
message.level = info['level']
822872

823-
def add_additional(span, text, level):
873+
def add_additional(span, text, level, suggested_replacement=None):
824874
child = Message()
825875
child.text = text
876+
child.suggested_replacement = suggested_replacement
826877
child.level = level
827878
child.primary = False
828879
if 'macros>' in span['file_name']:
829880
# Nowhere to display this, just send it to the console via msg_cb.
830881
msg_cb(child)
831882
else:
832883
child.path = make_span_path(span)
884+
if not os.path.exists(child.path):
885+
# Sometimes rust gives messages that link to libstd in the
886+
# directory where it was built (such as on Travis).
887+
return
833888
child.span = make_span_region(span)
834889
if any(map(lambda m: m.is_similar(child), message.children)):
835890
# Duplicate message, skip. This happens with some of the
836891
# macro help messages.
837-
return child
892+
return
838893
child.parent = message
839894
message.children.append(child)
840-
return child
841895

842896
if len(info['spans']) == 0:
843897
if parent_info:
@@ -925,11 +979,7 @@ def find_span_r(span, expansion=None):
925979
# Primary child message.
926980
add_additional(span, info['message'], info['level'])
927981
else:
928-
# Check if the main message is already set since there might
929-
# be multiple spans that are primary (in which case, we
930-
# arbitrarily show the main message on the first one).
931-
if not message.path:
932-
set_primary_message(span, info['message'])
982+
set_primary_message(span, info['message'])
933983

934984
label = span['label']
935985
# Some spans don't have a label. These seem to just imply
@@ -943,27 +993,11 @@ def find_span_r(span, expansion=None):
943993
if label is not None:
944994
# Display the label for this Span.
945995
add_additional(span, label, info['level'])
946-
if span['suggested_replacement']:
947-
# The "suggested_replacement" contains the code that
948-
# should replace the span.
949-
child = add_additional(span, None, 'help')
950-
replacement_template = util.multiline_fix("""
951-
<div class="rust-replacement"><a href="replace:%s" class="rust-button">Accept Replacement:</a> %s</div>
952-
""")
953-
html_suggestion = html.escape(span['suggested_replacement'], quote=False)
954-
if '\n' in html_suggestion:
955-
# Start on a new line so the text doesn't look too weird.
956-
html_suggestion = '\n' + html_suggestion
957-
html_suggestion = html_suggestion\
958-
.replace(' ', '&nbsp;')\
959-
.replace('\n', '<br>\n')
960-
child.minihtml_text = replacement_template % (
961-
urllib.parse.urlencode({
962-
'id': child.id,
963-
'replacement': span['suggested_replacement'],
964-
}),
965-
html_suggestion,
966-
)
996+
if span['suggested_replacement'] is not None:
997+
# The "suggested_replacement" contains the code that should
998+
# replace the span.
999+
add_additional(span, None, 'help',
1000+
suggested_replacement=span['suggested_replacement'])
9671001

9681002
# Recurse into children (which typically hold notes).
9691003
for child in info['children']:

rust/themes.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ def render(self, view, batch, for_popup=False):
9595
msgs = []
9696
last_level = None
9797
for i, msg in enumerate(batch):
98+
if msg.hidden:
99+
continue
98100
text = msg.escaped_text(view, '')
99101
if not text:
100102
continue
101-
if msg.minihtml_text:
103+
if msg.suggested_replacement is not None:
102104
level_text = ''
103105
else:
104106
if msg.level == last_level:
@@ -252,6 +254,8 @@ def icon(level):
252254
# Collect all the child messages together.
253255
children = []
254256
for child in batch.children:
257+
if child.hidden:
258+
continue
255259
# Don't show the icon for children with the same level as the
256260
# primary message.
257261
if isinstance(batch, PrimaryBatch) and child.level == batch.primary_message.level:
@@ -310,7 +314,7 @@ def render(self, view, batch, for_popup=False):
310314
for msg in batch:
311315
# Region-only messages will get checked by the region-checking
312316
# code.
313-
if msg.text or msg.minihtml_text:
317+
if msg.text or msg.suggested_replacement is not None:
314318
messages.append(msg)
315319

316320
# Create fake messages for the links to simplify the test code.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2017 The Rust Project Developers. See the COPYRIGHT
2+
// file at the top-level directory of this distribution and at
3+
// http://rust-lang.org/COPYRIGHT.
4+
//
5+
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8+
// option. This file may not be copied, modified, or distributed
9+
// except according to those terms.
10+
11+
use std::fmt::Debug;
12+
13+
trait Foo {
14+
fn foo(&self, _: &impl Debug);
15+
// ^^^^^^^^^^ERR(>=1.28.0-beta) declaration in trait here
16+
// ^^^^^^^^^^ERR(<1.28.0-beta) annotation in trait
17+
// ^^^^^^^^^^MSG See Primary: ↓:21
18+
}
19+
20+
impl Foo for () {
21+
fn foo<U: Debug>(&self, _: &U) { }
22+
// ^ERR method `foo` has incompatible signature
23+
// ^ERR(>=1.28.0-beta) expected `impl Trait`, found generic parameter
24+
// ^ERR(<1.28.0-beta) annotation in impl
25+
// ^MSG See Also: ↑:14
26+
// ^^^^^^^^^^HELP(>=1.28.0-beta) try removing the generic parameter
27+
// ^^^^^^^^^^HELP(>=1.28.0-beta) /Accept Replacement:</a> </div>/
28+
// ^HELP(>=1.28.0-beta) try removing the generic parameter
29+
// ^HELP(>=1.28.0-beta) /Accept Replacement:.*impl Debug/
30+
}
31+
32+
trait Bar {
33+
fn bar<U: Debug>(&self, _: &U);
34+
// ^ERR(>=1.28.0-beta) declaration in trait here
35+
// ^ERR(<1.28.0-beta) annotation in trait
36+
// ^MSG See Primary: ↓:40
37+
}
38+
39+
impl Bar for () {
40+
fn bar(&self, _: &impl Debug) { }
41+
// ^^^^^^^^^^ERR method `bar` has incompatible signature
42+
// ^^^^^^^^^^ERR(>=1.28.0-beta) expected generic parameter
43+
// ^^^^^^^^^^HELP(>=1.28.0-beta) try changing the `impl Trait` argument
44+
// ^^^^^^^^^^ERR(<1.28.0-beta) annotation in impl
45+
// ^^^^^^^^^^MSG See Also: ↑:33
46+
// |HELP(>=1.28.0-beta) try changing the `impl Trait` argument
47+
// |HELP(>=1.28.0-beta) /Accept Replacement:.*<U: Debug>/
48+
// ^^^^^^^^^^HELP(>=1.28.0-beta) /Accept Replacement:.*U</div>/
49+
}
50+
51+
// With non-local trait (#49841):
52+
53+
use std::hash::{Hash, Hasher};
54+
55+
struct X;
56+
57+
impl Hash for X {
58+
fn hash(&self, hasher: &mut impl Hasher) {}
59+
// ^^^^^^^^^^^ERR method `hash` has incompatible signature
60+
// ^^^^^^^^^^^ERR(>=1.28.0-beta) expected generic parameter
61+
// ^^^^^^^^^^^ERR(<1.28.0-beta) annotation in impl
62+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// This test is for a message with multiple primary spans
2+
// (both spans have no label).
3+
4+
#[repr(C, u64)]
5+
// ^WARN conflicting representation hints
6+
// ^^^WARN conflicting representation hints
7+
pub enum C { C }

0 commit comments

Comments
 (0)