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