diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 00000000..11bc76b8 --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,34 @@ +[ + { + + "id": "preferences", + "children": + [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": + [ + { + "caption": "Rust", + "children": + [ + { + "command": "open_file", + "args": {"file": "${packages}/Rust/Rust.sublime-settings"}, + "caption": "Settings – Default" + }, + { + "command": "open_file", + "args": {"file": "${packages}/User/Rust.sublime-settings"}, + "caption": "Settings – User" + } + ] + } + ] + } + ] + } + +] \ No newline at end of file diff --git a/Rust.sublime-settings b/Rust.sublime-settings index 7aa2d4a1..07c9f737 100644 --- a/Rust.sublime-settings +++ b/Rust.sublime-settings @@ -1,4 +1,4 @@ { // Enable the syntax checking plugin, which will highlight any errors during build - "rust_syntax_checking": false + "rust_syntax_checking": true } \ No newline at end of file diff --git a/plugin.py b/plugin.py index 35e06f66..9a6dacbd 100644 --- a/plugin.py +++ b/plugin.py @@ -1,85 +1,78 @@ import sublime, sublime_plugin import subprocess import os -import re +import html +import json - -def is_event_on_gutter(view, event): - """Determine if a mouse event points to the gutter. - - Because this is inapplicable for empty lines, - returns `None` to let the caller decide on what do to. - """ - original_pt = view.window_to_text((event["x"], event["y"])) - if view.rowcol(original_pt)[1] != 0: - return False - - # If the line is empty, - # we will always get the same textpos - # regardless of x coordinate. - # Return `None` in this case and let the caller decide. - if view.line(original_pt).empty(): - return None - - # ST will put the caret behind the first character - # if we click on the second half of the char. - # Use view.em_width() / 2 to emulate this. - adjusted_pt = view.window_to_text((event["x"] + view.em_width() / 2, event["y"])) - if adjusted_pt != original_pt: - return False - - return original_pt - - -def callback(test): - pass - class rustPluginSyntaxCheckEvent(sublime_plugin.EventListener): - def __init__(self): - # This will fetch the line number that failed from the $ cargo run output - # We could fetch multiple lines but this is a start - # Lets compile it here so we don't need to compile on every save - self.lineRegex = re.compile(b"(\w*\.rs):(\d+).*error\:\s(.*)") - self.errors = {} - - def get_line_number_and_msg(self, output): - if self.lineRegex.search(output): - return self.lineRegex.search(output) - - def draw_dots_to_screen(self, view, line_num): - line_num -= 1 # line numbers are zero indexed on the sublime API, so take off 1 - view.add_regions('buildError', [view.line(view.text_point(line_num, 0))], 'comment', 'dot', sublime.HIDDEN) - - def on_post_save_async(self, view): - if "source.rust" in view.scope_name(0) and view.settings().get('rust_syntax_checking'): # Are we in rust scope and is it switched on? - self.errors = {} # reset on every save - view.erase_regions('buildError') + # Are we in rust scope and is it switched on? + # We use phantoms which were added in 3118 + enabled = view.settings().get('rust_syntax_checking') and int(sublime.version()) >= 3118 + if "source.rust" in view.scope_name(0) and enabled: os.chdir(os.path.dirname(view.file_name())) # shell=True is needed to stop the window popping up, although it looks like this is needed: http://stackoverflow.com/questions/3390762/how-do-i-eliminate-windows-consoles-from-spawned-processes-in-python-2-7 # We only care about stderr - cargoRun = subprocess.Popen('cargo rustc -- -Zno-trans', shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE) + cargoRun = subprocess.Popen('cargo rustc -- -Zno-trans -Zunstable-options --error-format=json', + shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, + universal_newlines = True + ) output = cargoRun.communicate() - result = self.get_line_number_and_msg(output[1]) if len(output) > 1 else False - if (result): - fileName = result.group(1).decode('utf-8') - view_filename = os.path.basename(view.file_name()) - line = int(result.group(2)) - msg = result.group(3).decode('utf-8') - if (fileName == view_filename and line): - self.errors[line] = msg - self.draw_dots_to_screen(view, int(line)) - else: - view.erase_regions('buildError') - + view.erase_phantoms('buildErrorLine') + + for line in output[1].split('\n'): + if line == '' or line[0] != '{': + continue + info = json.loads(line) + # Can't show without spans + if len(info['spans']) == 0: + continue + self.add_error_phantom(view, info) + + def add_error_phantom(self, view, info): + msg = info['message'] + + base_color = "#F00" # Error color + if info['level'] != "error": + # Warning color + base_color = "#FF0" + + view_filename = view.file_name() + for span in info['spans']: + if not view_filename.endswith(span['file_name']): + continue + color = base_color + char = "^" + if not span['is_primary']: + # Non-primary spans are normally additional + # information to help understand the error. + color = "#0FF" + char = "-" + # Sublime text is 0 based whilst the line/column info from + # rust is 1 based. + area = sublime.Region( + view.text_point(span['line_start'] - 1, span['column_start'] - 1), + view.text_point(span['line_end'] - 1, span['column_end'] - 1) + ) + + underline = char * (span['column_end'] - span['column_start']) + label = span['label'] + if not label: + label = '' + + view.add_phantom( + 'buildErrorLine', area, + "{} {}" + .format(color, underline, html.escape(label, quote=False)), + sublime.LAYOUT_BELOW + ) + if span['is_primary']: + view.add_phantom( + 'buildErrorLine', area, + "{}" + .format(color, html.escape(msg, quote=False)), + sublime.LAYOUT_BELOW + ) - def on_text_command(self, view, command_name, args): - if (args and 'event' in args): - event = args['event'] - else: - return - if (is_event_on_gutter(view, event)): - line_clicked = view.rowcol(is_event_on_gutter(view, event))[0] + 1 - view.show_popup_menu([self.errors[line_clicked]], callback) \ No newline at end of file