"""Module for compile error visualization. Attributes: log (logging): this module logger """ import logging import sublime from os import path from ..completion.compiler_variant import LibClangCompilerVariant from ..settings.settings_storage import SettingsStorage from ..utils.subl.row_col import ZeroIndexedRowCol from .popups import Popup log = logging.getLogger("ECC") PATH_TO_ICON = "Packages/EasyClangComplete/pics/icons/{icon}" MIN_ERROR_SEVERITY = 3 class PopupErrorVis: """A class for compile error visualization with popups. Attributes: err_regions (dict): dictionary of error regions for view ids """ _TAG_ERRORS = "easy_clang_complete_errors" _TAG_WARNINGS = "easy_clang_complete_warnings" _ERROR_SCOPE = "undefined" _WARNING_SCOPE = "undefined" def __init__(self, settings): """Initialize error visualization. Args: mark_gutter (bool): add a mark to the gutter for error regions """ gutter_style = settings.gutter_style mark_style = settings.linter_mark_style self.settings = settings self.err_regions = {} if gutter_style == SettingsStorage.GUTTER_COLOR_STYLE: self.gutter_mark_error = PATH_TO_ICON.format( icon="error.png") self.gutter_mark_warning = PATH_TO_ICON.format( icon="warning.png") elif gutter_style == SettingsStorage.GUTTER_MONO_STYLE: self.gutter_mark_error = PATH_TO_ICON.format( icon="error_mono.png") self.gutter_mark_warning = PATH_TO_ICON.format( icon="warning_mono.png") elif gutter_style == SettingsStorage.GUTTER_DOT_STYLE: self.gutter_mark_error = PATH_TO_ICON.format( icon="error_dot.png") self.gutter_mark_warning = PATH_TO_ICON.format( icon="warning_dot.png") else: log.error("Unknown option for gutter_style: %s", gutter_style) self.gutter_mark_error = "" self.gutter_mark_warning = "" if mark_style == SettingsStorage.MARK_STYLE_OUTLINE: self.draw_flags = sublime.DRAW_EMPTY | sublime.DRAW_NO_FILL elif mark_style == SettingsStorage.MARK_STYLE_FILL: self.draw_flags = 0 elif mark_style == SettingsStorage.MARK_STYLE_SOLID_UNDERLINE: self.draw_flags = sublime.DRAW_NO_FILL | \ sublime.DRAW_NO_OUTLINE | sublime.DRAW_SOLID_UNDERLINE elif mark_style == SettingsStorage.MARK_STYLE_STIPPLED_UNDERLINE: self.draw_flags = sublime.DRAW_NO_FILL | \ sublime.DRAW_NO_OUTLINE | sublime.DRAW_STIPPLED_UNDERLINE elif mark_style == SettingsStorage.MARK_STYLE_SQUIGGLY_UNDERLINE: self.draw_flags = sublime.DRAW_NO_FILL | \ sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE else: self.draw_flags = sublime.HIDDEN def generate(self, view, errors): """Generate a dictionary that stores all errors. The errors are stored along with their positions and descriptions. Needed to show these errors on the screen. Args: view (sublime.View): current view errors (list): list of parsed errors (dict objects) """ view_id = view.buffer_id() if view_id == 0: log.error("Trying to show error on invalid view. Abort.") return log.debug("Generating error regions for view %s", view_id) # first clear old regions if view_id in self.err_regions: log.debug("Removing old error regions") del self.err_regions[view_id] # create an empty region dict for view id self.err_regions[view_id] = {} # If the view is closed while this is running, there will be # errors. We want to handle them gracefully. try: for error in errors: self.add_error(view, error) log.debug("%s error regions ready", len(self.err_regions)) except (AttributeError, KeyError, TypeError) as e: log.error("View was closed -> cannot generate error vis in it") log.info("Original exception: '%s'", repr(e)) def add_error(self, view, error_dict): """Put new compile error in the dictionary of errors. Args: view (sublime.View): current view error_dict (dict): current error dict {row, col, file, region} """ logging.debug("Adding error %s", error_dict) error_source_file = path.basename(error_dict['file']) if error_source_file == path.basename(view.file_name()): row_col = ZeroIndexedRowCol(error_dict['row'], error_dict['col']) point = row_col.as_1d_location(view) error_dict['region'] = view.word(point) if row_col.row in self.err_regions[view.buffer_id()]: self.err_regions[view.buffer_id()][row_col.row] += [error_dict] else: self.err_regions[view.buffer_id()][row_col.row] = [error_dict] def show_errors(self, view): """Show current error regions. Args: view (sublime.View): Current view """ if view.buffer_id() not in self.err_regions: # view has no errors for it return current_error_dict = self.err_regions[view.buffer_id()] error_regions, warning_regions = PopupErrorVis._as_region_list( current_error_dict) log.debug("Showing error regions: %s", error_regions) log.debug("Showing warning regions: %s", warning_regions) view.add_regions( key=PopupErrorVis._TAG_ERRORS, regions=error_regions, scope=PopupErrorVis._ERROR_SCOPE, icon=self.gutter_mark_error, flags=self.draw_flags) view.add_regions( key=PopupErrorVis._TAG_WARNINGS, regions=warning_regions, scope=PopupErrorVis._WARNING_SCOPE, icon=self.gutter_mark_warning, flags=self.draw_flags) def erase_regions(self, view): """Erase error regions for view. Args: view (sublime.View): erase regions for view """ if view.buffer_id() not in self.err_regions: # view has no errors for it return log.debug("Erasing error regions for view %s", view.buffer_id()) view.erase_regions(PopupErrorVis._TAG_ERRORS) view.erase_regions(PopupErrorVis._TAG_WARNINGS) def show_popup_if_needed(self, view, row): """Show a popup if it is needed in this row. Args: view (sublime.View): current view row (int): number of row """ if view.buffer_id() not in self.err_regions: return current_err_region_dict = self.err_regions[view.buffer_id()] if row in current_err_region_dict: errors_dict = current_err_region_dict[row] max_severity, error_list = PopupErrorVis._as_msg_list(errors_dict) text_to_show = PopupErrorVis.__to_md(error_list) if max_severity < MIN_ERROR_SEVERITY: popup = Popup.warning(text_to_show, self.settings) else: popup = Popup.error(text_to_show, self.settings) popup.show(view) else: log.debug("No error regions for row: %s", row) def clear(self, view): """Clear errors from dict for view. Args: view (sublime.View): current view """ if view.buffer_id() not in self.err_regions: # no errors for this view return view.hide_popup() self.erase_regions(view) del self.err_regions[view.buffer_id()] @staticmethod def _as_msg_list(errors_dicts): """Return errors as list. Args: errors_dicts (dict[]): A list of error dicts """ error_list = [] max_severity = 0 for entry in errors_dicts: error_list.append(entry['error']) if LibClangCompilerVariant.SEVERITY_TAG in entry: severity = entry[LibClangCompilerVariant.SEVERITY_TAG] if severity > max_severity: max_severity = severity return max_severity, error_list @staticmethod def _as_region_list(err_regions_dict): """Make a list from error region dict. Args: err_regions_dict (dict): dict of error regions for current view Returns: list(Region): list of regions to show on sublime view """ errors = [] warnings = [] for errors_list in err_regions_dict.values(): for entry in errors_list: severity = MIN_ERROR_SEVERITY if LibClangCompilerVariant.SEVERITY_TAG in entry: severity = entry[LibClangCompilerVariant.SEVERITY_TAG] if severity < MIN_ERROR_SEVERITY: warnings.append(entry['region']) else: errors.append(entry['region']) return errors, warnings @staticmethod def __to_md(error_list): """Convert an error dict to markdown string.""" if len(error_list) > 1: # Make it a markdown list. text_to_show = '\n- '.join(error_list) text_to_show = '- ' + text_to_show else: text_to_show = error_list[0] return text_to_show