diff --git a/plugin.py b/plugin.py deleted file mode 100644 index 35e06f66..00000000 --- a/plugin.py +++ /dev/null @@ -1,85 +0,0 @@ -import sublime, sublime_plugin -import subprocess -import os -import re - - -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') - 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) - 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') - - - 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 diff --git a/syntaxCheckerPlugin.py b/syntaxCheckerPlugin.py new file mode 100644 index 00000000..a99599cd --- /dev/null +++ b/syntaxCheckerPlugin.py @@ -0,0 +1,130 @@ +import sublime, sublime_plugin +import subprocess +import os +import re +import webbrowser + +# Requires Sublime Text 3 for the new pop ups API +is_enabled = sublime.load_settings("Rust.sublime-settings").get("rust_syntax_checking") and sublime.version() != 3113 + + +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 + + +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.findall(output): + return self.lineRegex.findall(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_dot_' + str(line_num), [view.line(view.text_point(line_num, 0))], 'comment', 'dot', sublime.HIDDEN) + + # Callback for when a link is clicked within a popup + def on_navigate(self, href): + webbrowser.open(href) + + # If there's an easier way of doing this please let me know + def clear_all_regions(self, view): + # THis is annoying as there's no "get all lines" in the API, so i have to create a region which covers the whole file, then generate an array, so i have a number + # Once i have that number i can create a for-loop and remove any regions hanging around. It would be nice to just have a clearRegions() method + total_lines = view.lines(sublime.Region(0, view.size())) + for i, v in enumerate(total_lines): + # The lines here will be zero-indexed, so bump up the num + i = str(i + 1) + view.erase_regions('buildError_highlight_' + i) + view.erase_regions('buildError_dot_' + i) + + + def on_post_save_async(self, view): + if "source.rust" in view.scope_name(0) and is_enabled: # Are we in rust scope and is it switched on? + # reset on every save + self.errors = {} + self.clear_all_regions(view) + + 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) + output = cargoRun.communicate() + results = self.get_line_number_and_msg(output[1]) if len(output) > 1 else False + if (results): + # There could be more than 1 error, so traverse through + for result in results: + fileName = result[0].decode('utf-8') + view_filename = os.path.basename(view.file_name()) + line = int(result[1]) + msg = result[2].decode('utf-8') + if (fileName == view_filename and line): + self.errors[line] = {} + self.errors[line]['msg'] = msg + if self.parse_error_message(self.errors[line]['msg']): # Were we able to get a token from this error? + token = self.parse_error_message(self.errors[line]['msg']) + self.errors[line]['token_region'] = view.find(token, view.text_point(line - 1, 0)) # this converts the token into a region + view.add_regions("buildError_highlight_" + str(line), [self.errors[line]['token_region']], 'comment') + + + self.draw_dots_to_screen(view, int(line)) + + # This method will try to parse the error message and return the illegal token so the editor can highlight it + def parse_error_message(self, error_message): + # Im sure more error matching could be added here, but ill leave it as this for now + expected_found_regex = re.compile("found `(.*)`") + type_name_regex = re.compile("name `(.*)`") + token = False + if expected_found_regex.search(error_message): + token = expected_found_regex.search(error_message).group(1) + elif type_name_regex.search(error_message): + token = type_name_regex.search(error_message).group(1) + + return token + + def on_text_command(self, view, command_name, args): + if "source.rust" in view.scope_name(0) and is_enabled: + 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 + if line_clicked in self.errors: + view.show_popup(self.errors[line_clicked]['msg']) + + clicked_point = view.window_to_text((event["x"], event["y"])) + clicked_row = view.rowcol(clicked_point)[0] + 1 + if clicked_row in self.errors: + styled_error_message = re.sub(r'\[(.*)\]$', r'
\1', self.errors[clicked_row]['msg']) + view.show_popup(styled_error_message, location=clicked_point, on_navigate=self.on_navigate)