From 1c0a64ed8ce6b1119f71e5aaaefdb3a45fb3888f Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:13:22 +1000 Subject: [PATCH 01/41] Add Show Ruby Coverage command to Command Palette. --- SublimeRubyCoverage.sublime-commands | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 SublimeRubyCoverage.sublime-commands diff --git a/SublimeRubyCoverage.sublime-commands b/SublimeRubyCoverage.sublime-commands new file mode 100644 index 0000000..8c992e2 --- /dev/null +++ b/SublimeRubyCoverage.sublime-commands @@ -0,0 +1,3 @@ +[ + { "caption": "Show Ruby Coverage", "command": "show_ruby_coverage"} +] From fe582e80a5e8cb51db80d083691cb515e86afe3c Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:32:56 +1000 Subject: [PATCH 02/41] Move utility functions to the end of the file. --- SublimeRubyCoverage.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/SublimeRubyCoverage.py b/SublimeRubyCoverage.py index 9ff85e8..415e44c 100644 --- a/SublimeRubyCoverage.py +++ b/SublimeRubyCoverage.py @@ -2,23 +2,6 @@ import sublime import sublime_plugin import re -PLUGIN_FILE = os.path.abspath(__file__) - -def find_project_root(file_path): - """Project Root is defined as the parent directory that contains a directory called 'coverage'""" - if os.access(os.path.join(file_path, 'coverage'), os.R_OK): - return file_path - - parent, current = os.path.split(file_path) - if current: - return find_project_root(parent) - -def explode_path(path): - first, second = os.path.split(path) - if second: - return explode_path(first) + [second] - else: - return [first] class SublimeRubyCoverageListener(sublime_plugin.EventListener): """Event listener to highlight uncovered lines when a Ruby file is loaded.""" @@ -93,3 +76,20 @@ def file_exempt(self, filename): if re.compile(pattern).search(normalized_filename) is not None: return True return False + +def find_project_root(file_path): + """Project Root is defined as the parent directory that contains a directory called 'coverage'""" + if os.access(os.path.join(file_path, 'coverage'), os.R_OK): + return file_path + + parent, current = os.path.split(file_path) + if current: + return find_project_root(parent) + +def explode_path(path): + first, second = os.path.split(path) + if second: + return explode_path(first) + [second] + else: + return [first] + From 88bb71a9c0c39fd5bfb6798683eb48f6e9f13c5c Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:39:42 +1000 Subject: [PATCH 03/41] Simplify language of comment and status message. --- SublimeRubyCoverage.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SublimeRubyCoverage.py b/SublimeRubyCoverage.py index 415e44c..7260a8c 100644 --- a/SublimeRubyCoverage.py +++ b/SublimeRubyCoverage.py @@ -4,7 +4,7 @@ import re class SublimeRubyCoverageListener(sublime_plugin.EventListener): - """Event listener to highlight uncovered lines when a Ruby file is loaded.""" + """Show coverage when a Ruby file is loaded.""" def on_load(self, view): if 'source.ruby' not in view.scope_name(0): @@ -51,7 +51,7 @@ def run(self, edit): outlines.append(sublime.Region(0,view.size())) view.set_status('SublimeRubyCoverage', 'UNCOVERED!') if view.window(): - sublime.status_message("Oh dear. We can't seem to find the coverage file. We tried looking here: " + coverage_filepath + ", but then we gave up.") + sublime.status_message("Coverage file not found: " + coverage_filepath) # update highlighted regions if outlines: From 6e3edaa9ec32f782438b0f84f08aeab153ca32c7 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:43:23 +1000 Subject: [PATCH 04/41] Simplify EventListner class name. --- SublimeRubyCoverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SublimeRubyCoverage.py b/SublimeRubyCoverage.py index 7260a8c..9556fe4 100644 --- a/SublimeRubyCoverage.py +++ b/SublimeRubyCoverage.py @@ -3,7 +3,7 @@ import sublime_plugin import re -class SublimeRubyCoverageListener(sublime_plugin.EventListener): +class ShowRubyCoverageListener(sublime_plugin.EventListener): """Show coverage when a Ruby file is loaded.""" def on_load(self, view): From a3924b00b23661371c541edc17d9f1f540181e7d Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:43:33 +1000 Subject: [PATCH 05/41] Remove debug output. --- SublimeRubyCoverage.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/SublimeRubyCoverage.py b/SublimeRubyCoverage.py index 9556fe4..e181ffc 100644 --- a/SublimeRubyCoverage.py +++ b/SublimeRubyCoverage.py @@ -60,7 +60,6 @@ def run(self, edit): def file_exempt(self, filename): normalized_filename = os.path.normpath(filename).replace('\\', '/') - print(normalized_filename) exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', r'\.gemspec'] @@ -72,7 +71,6 @@ def file_exempt(self, filename): exempt.append(path) for pattern in exempt: - print(pattern) if re.compile(pattern).search(normalized_filename) is not None: return True return False From a498709062d2a8931a85576491b3160539d8f646 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 21:47:35 +1000 Subject: [PATCH 06/41] Rename main code file in preparation for creating additional files. --- SublimeRubyCoverage.py => show_ruby_coverage.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SublimeRubyCoverage.py => show_ruby_coverage.py (100%) diff --git a/SublimeRubyCoverage.py b/show_ruby_coverage.py similarity index 100% rename from SublimeRubyCoverage.py rename to show_ruby_coverage.py From 67b3188b154f496ac236f0efaaf75d173d7090fc Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 22:30:25 +1000 Subject: [PATCH 07/41] Simplify function comment. --- show_ruby_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/show_ruby_coverage.py b/show_ruby_coverage.py index e181ffc..dbdfbad 100644 --- a/show_ruby_coverage.py +++ b/show_ruby_coverage.py @@ -76,7 +76,7 @@ def file_exempt(self, filename): return False def find_project_root(file_path): - """Project Root is defined as the parent directory that contains a directory called 'coverage'""" + """the parent directory that contains a directory called 'coverage'""" if os.access(os.path.join(file_path, 'coverage'), os.R_OK): return file_path From ae61b1ea0b9bd52f4c11ce538e6bd428b8f253ef Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 22:32:45 +1000 Subject: [PATCH 08/41] Highlight uncovered lines, rather than using gutter icons. --- show_ruby_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/show_ruby_coverage.py b/show_ruby_coverage.py index dbdfbad..7c8f2ac 100644 --- a/show_ruby_coverage.py +++ b/show_ruby_coverage.py @@ -56,7 +56,7 @@ def run(self, edit): # update highlighted regions if outlines: view.add_regions('SublimeRubyCoverage', outlines, - 'coverage.uncovered', 'bookmark', sublime.HIDDEN) + 'coverage.uncovered') def file_exempt(self, filename): normalized_filename = os.path.normpath(filename).replace('\\', '/') From 7203dc44463a27aefec0371b185557431984839b Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Fri, 12 Jun 2015 22:50:18 +1000 Subject: [PATCH 09/41] Instead of showing coverage on load, make command toggle it on/off. --- Default.sublime-keymap | 2 +- SublimeRubyCoverage.sublime-commands | 2 +- ...uby_coverage.py => toggle_ruby_coverage.py | 40 ++++++++++--------- 3 files changed, 24 insertions(+), 20 deletions(-) rename show_ruby_coverage.py => toggle_ruby_coverage.py (76%) diff --git a/Default.sublime-keymap b/Default.sublime-keymap index cdb3ccd..1187d82 100644 --- a/Default.sublime-keymap +++ b/Default.sublime-keymap @@ -1,3 +1,3 @@ [ - { "keys": ["super+shift+c"], "command": "show_ruby_coverage"} + { "keys": ["super+shift+c"], "command": "toggle_ruby_coverage"} ] diff --git a/SublimeRubyCoverage.sublime-commands b/SublimeRubyCoverage.sublime-commands index 8c992e2..3f1f3fc 100644 --- a/SublimeRubyCoverage.sublime-commands +++ b/SublimeRubyCoverage.sublime-commands @@ -1,3 +1,3 @@ [ - { "caption": "Show Ruby Coverage", "command": "show_ruby_coverage"} + { "caption": "View: Toggle Ruby Coverage", "command": "toggle_ruby_coverage"} ] diff --git a/show_ruby_coverage.py b/toggle_ruby_coverage.py similarity index 76% rename from show_ruby_coverage.py rename to toggle_ruby_coverage.py index 7c8f2ac..3fa3545 100644 --- a/show_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -3,19 +3,22 @@ import sublime_plugin import re -class ShowRubyCoverageListener(sublime_plugin.EventListener): - """Show coverage when a Ruby file is loaded.""" +class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): + """Show/hide coverage of current file based on a previous coverage run.""" - def on_load(self, view): - if 'source.ruby' not in view.scope_name(0): + def run(self, edit): + if 'source.ruby' not in self.view.scope_name(0): return - view.run_command('show_ruby_coverage') - -class ShowRubyCoverageCommand(sublime_plugin.TextCommand): - """Highlight uncovered lines in the current file based on a previous coverage run.""" + settings = self.view.settings() + if settings.has('ruby_coverage.visible'): + self.hide_coverage() + settings.erase('ruby_coverage.visible') + else: + self.show_coverage() + settings.set('ruby_coverage.visible', True) - def run(self, edit): + def show_coverage(self): view = self.view filename = view.file_name() if not filename: @@ -34,30 +37,31 @@ def run(self, edit): coverage_filename = '-'.join(explode_path(relative_file_path))[1:].replace(".rb", "_rb.csv").replace(".y", "_y.csv") coverage_filepath = os.path.join(project_root, 'coverage', 'sublime-ruby-coverage', coverage_filename) - # Clean up - view.erase_status('SublimeRubyCoverage') - view.erase_regions('SublimeRubyCoverage') - - outlines = [] + regions = [] try: with open(coverage_filepath) as coverage_file: for current_line, line in enumerate(coverage_file): if line.strip() != '1': region = view.full_line(view.text_point(current_line, 0)) - outlines.append(region) + regions.append(region) except IOError as e: # highlight the entire view - outlines.append(sublime.Region(0,view.size())) + regions.append(sublime.Region(0,view.size())) view.set_status('SublimeRubyCoverage', 'UNCOVERED!') if view.window(): sublime.status_message("Coverage file not found: " + coverage_filepath) # update highlighted regions - if outlines: - view.add_regions('SublimeRubyCoverage', outlines, + if regions: + view.add_regions('SublimeRubyCoverage', regions, 'coverage.uncovered') + def hide_coverage(self): + view = self.view + view.erase_status('SublimeRubyCoverage') + view.erase_regions('SublimeRubyCoverage') + def file_exempt(self, filename): normalized_filename = os.path.normpath(filename).replace('\\', '/') From 27a7b782fe409082f4f56624faf5fdb38462cee4 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 10:57:40 +1000 Subject: [PATCH 10/41] Use output of simplecov-json formatter, not simplecov-sublime-ruby-coverage. The Simplecov package for the Atom editor uses this format. By using it here, we can support teams who use both editors without requiring them to use multiple formatters. --- README.md | 8 ++-- toggle_ruby_coverage.py | 85 +++++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index b1d95a1..d729cd4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,19 @@ SublimeRubyCoverage ==================== -A plugin for Sublime Text 2 that can highlight lines of Ruby lacking test coverage. +A plugin for Sublime Text 2/3 that can highlight lines of Ruby lacking test coverage. Installation ------------ -You will need to setup [simplecov-sublime-ruby-coverage](http://github.com/integrum/simplecov-sublime-ruby-coverage) in your project. +You will need to set up the [simplecov-json](https://github.com/vicentllongo/simplecov-json) Simplecov formatter in your project. -Set up [Sublime Package Control](http://wbond.net/sublime_packages/package_control) -if you don't have it yet. +Set up [Sublime Package Control](http://wbond.net/sublime_packages/package_control) if you don't have it yet. Go to Tools > Command Palette. Type `Package Control: Install Package` and hit enter. Type `Ruby Coverage` and hit enter. - Usage ----- diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 3fa3545..7d58560 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -1,6 +1,7 @@ import os import sublime import sublime_plugin +import json import re class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): @@ -20,43 +21,36 @@ def run(self, edit): def show_coverage(self): view = self.view - filename = view.file_name() - if not filename: - return - if self.file_exempt(filename): + filename = self.get_filename() + if not filename: return - project_root = find_project_root(filename) - if not project_root: - print('Could not find coverage directory.') + coverage = get_coverage_for_filename(filename) + if not coverage: + regions.append(sublime.Region(0,view.size())) + view.set_status('SublimeRubyCoverage', 'NOT COVERED') + if view.window(): + sublime.status_message('No coverage data for this file.') return - - relative_file_path = os.path.relpath(filename, project_root) - - coverage_filename = '-'.join(explode_path(relative_file_path))[1:].replace(".rb", "_rb.csv").replace(".y", "_y.csv") - coverage_filepath = os.path.join(project_root, 'coverage', 'sublime-ruby-coverage', coverage_filename) - regions = [] - try: - with open(coverage_filepath) as coverage_file: - for current_line, line in enumerate(coverage_file): - if line.strip() != '1': - region = view.full_line(view.text_point(current_line, 0)) - regions.append(region) - except IOError as e: - # highlight the entire view - regions.append(sublime.Region(0,view.size())) - view.set_status('SublimeRubyCoverage', 'UNCOVERED!') - if view.window(): - sublime.status_message("Coverage file not found: " + coverage_filepath) + for line_number, line_coverage in list(enumerate(coverage)): + if line_coverage == 0: + regions.append(view.full_line(view.text_point(line_number, 0))) # update highlighted regions if regions: view.add_regions('SublimeRubyCoverage', regions, 'coverage.uncovered') + def get_filename(self): + view = self.view + filename = view.file_name() + if not filename or self.file_exempt(filename): + return + return filename + def hide_coverage(self): view = self.view view.erase_status('SublimeRubyCoverage') @@ -68,7 +62,7 @@ def file_exempt(self, filename): exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', r'\.gemspec'] - root = find_project_root(self.view.file_name()) + root = get_project_root_directory(self.view.file_name()) ignore = os.path.join(root, '.covignore') if os.path.isfile(ignore): for path in open(ignore).read().rstrip("\n").split("\n"): @@ -79,14 +73,41 @@ def file_exempt(self, filename): return True return False -def find_project_root(file_path): +def get_coverage_for_filename(filename): + coverage = get_coverage(filename) + coverage_files = coverage['files'] + for coverage_file in coverage_files: + if coverage_file['filename'] == filename: + return coverage_file['coverage'] + +def get_coverage(filename): + filename = get_coverage_filename(filename) + with open(filename) as json_file: + return json.load(json_file) + +def get_coverage_filename(filename): + project_root = get_project_root_directory(filename) + if not project_root: + return + + coverage_filename = os.path.join(project_root, 'coverage', 'coverage.json') + if not os.access(coverage_filename, os.R_OK): + print('Could not find coverage.json file.') + return + + return coverage_filename + +def get_project_root_directory(filename): """the parent directory that contains a directory called 'coverage'""" - if os.access(os.path.join(file_path, 'coverage'), os.R_OK): - return file_path + coverage_directory = os.path.join(filename, 'coverage') + if os.access(coverage_directory, os.R_OK): + return filename + + parent, current = os.path.split(filename) + if not current: + print('Could not find coverage directory.') - parent, current = os.path.split(file_path) - if current: - return find_project_root(parent) + return get_project_root_directory(parent) def explode_path(path): first, second = os.path.split(path) From edebcd879bda81e25edb8b57789e2d71c82d609e Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 11:44:06 +1000 Subject: [PATCH 11/41] Augment active color scheme with colors for coverage view. --- SublimeRubyCoverage.sublime-settings | 28 +++++++++ theme_generator.py | 89 ++++++++++++++++++++++++++++ toggle_ruby_coverage.py | 56 ++++++++++++++++- 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 SublimeRubyCoverage.sublime-settings create mode 100644 theme_generator.py diff --git a/SublimeRubyCoverage.sublime-settings b/SublimeRubyCoverage.sublime-settings new file mode 100644 index 0000000..df6056f --- /dev/null +++ b/SublimeRubyCoverage.sublime-settings @@ -0,0 +1,28 @@ +{ + /* + Change this to `true` to scroll to the first uncovered line + automatically when you activate the coverage view. + */ + "auto_scoll_to_uncovered": false, + + "colors": { + /* + Colors used to display coverage. + */ + "coverage": { + "uncovered_foreground": "#F9F9F4", + "uncovered_background": "#A83732", + "covered_foreground": "#F9F9F4", + "covered_background": "#37A832", + "covered_foreground_bold": "#F9F9F4", + "covered_background_bold": "#287020", + "covered_foreground_extrabold": "#F9F9F4", + "covered_background_extrabold": "#287020" + } + }, + + /* + Change this to `false` to suppress coverage status in status bar. + */ + "coverage_status_in_status_bar": true +} diff --git a/theme_generator.py b/theme_generator.py new file mode 100644 index 0000000..2ff32b9 --- /dev/null +++ b/theme_generator.py @@ -0,0 +1,89 @@ +""" +Given the resource path to a Sublime theme file, generate a new +theme and allow the consumer to augment this theme and apply it +to a view. + +(Hat tip to GitSavvy for this technique!) +""" + +import os +from xml.etree import ElementTree + +import sublime + + +STYLES_HEADER = """ + + +""" + +STYLE_TEMPLATE = """ + + name + {name} + scope + {scope} + settings + +{properties} + + +""" + +PROPERTY_TEMPLATE = """ + {key} + {value} +""" + + +class ThemeGenerator(): + + """ + Given the path to a `.tmTheme` file, parse it, allow transformations + on the data, save it, and apply the transformed theme to a view. + """ + + def __init__(self, original_color_scheme): + color_scheme_xml = sublime.load_resource(original_color_scheme) + self.plist = ElementTree.XML(color_scheme_xml) + self.styles = self.plist.find("./dict/array") + + def add_scoped_style(self, name, scope, **kwargs): + """ + Add scope-specific styles to the theme. A unique name should be provided + as well as a scope corresponding to regions of text. Any keyword arguments + will be used as key and value for the newly-defined style. + """ + properties = "".join(PROPERTY_TEMPLATE.format(key=k, value=v) for k, v in kwargs.items()) + new_style = STYLE_TEMPLATE.format(name=name, scope=scope, properties=properties) + self.styles.append(ElementTree.XML(new_style)) + + def get_new_theme_path(self, name): + """ + Save the transformed theme to disk and return the path to that theme, + relative to the Sublime packages directory. + """ + if not os.path.exists(os.path.join(sublime.packages_path(), "User", "SublimeRubyCoverage")): + os.makedirs(os.path.join(sublime.packages_path(), "User", "SublimeRubyCoverage")) + + path_in_packages = os.path.join("User", + "SublimeRubyCoverage", + "SublimeRubyCoverage.{}.tmTheme".format(name)) + + full_path = os.path.join(sublime.packages_path(), path_in_packages) + + with open(full_path, "wb") as out_f: + out_f.write(STYLES_HEADER.encode("utf-8")) + out_f.write(ElementTree.tostring(self.plist, encoding="utf-8")) + + return path_in_packages + + def apply_new_theme(self, name, target_view): + """ + Apply the transformed theme to the specified target view. + """ + path_in_packages = self.get_new_theme_path(name) + + # Sublime expects `/`-delimited paths, even in Windows. + theme_path = os.path.join("Packages", path_in_packages).replace("\\", "/") + target_view.settings().set("color_scheme", theme_path) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 7d58560..11fcda4 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -4,6 +4,8 @@ import json import re +from .theme_generator import ThemeGenerator + class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): """Show/hide coverage of current file based on a previous coverage run.""" @@ -39,8 +41,10 @@ def show_coverage(self): if line_coverage == 0: regions.append(view.full_line(view.text_point(line_number, 0))) - # update highlighted regions if regions: + file_ext = get_file_extension(os.path.basename(filename)) + augment_color_scheme(view, file_ext) + view.add_regions('SublimeRubyCoverage', regions, 'coverage.uncovered') @@ -53,6 +57,7 @@ def get_filename(self): def hide_coverage(self): view = self.view + restore_color_scheme(view) view.erase_status('SublimeRubyCoverage') view.erase_regions('SublimeRubyCoverage') @@ -73,6 +78,52 @@ def file_exempt(self, filename): return True return False +def augment_color_scheme(view, file_ext): + """ + Given a target view, generate a new color scheme from the original with + additional coverage-related style rules added. Save this color scheme to + disk and set it as the target view's active color scheme. + + (Hat tip to GitSavvy for this technique!) + """ + colors = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("colors") + + settings = view.settings() + original_color_scheme = settings.get("color_scheme") + settings.set("ruby_coverage.original_color_scheme", original_color_scheme) + themeGenerator = ThemeGenerator(original_color_scheme) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Uncovered Line", + "coverage.uncovered", + background=colors["coverage"]["uncovered_background"], + foreground=colors["coverage"]["uncovered_foreground"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Covered Line", + "coverage.covered", + background=colors["coverage"]["covered_background"], + foreground=colors["coverage"]["covered_foreground"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage More Covered Line", + "coverage.covered.more", + background=colors["coverage"]["covered_background_bold"], + foreground=colors["coverage"]["covered_foreground_bold"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Most Covered Line", + "coverage.covered.most", + background=colors["coverage"]["covered_background_extrabold"], + foreground=colors["coverage"]["covered_foreground_extrabold"] + ) + themeGenerator.apply_new_theme("ruby-coverage-view." + file_ext, view) + +def restore_color_scheme(view): + settings = view.settings() + original_color_scheme = settings.get("ruby_coverage.original_color_scheme") + settings.set("color_scheme", original_color_scheme) + settings.erase("ruby_coverage.original_color_scheme") + def get_coverage_for_filename(filename): coverage = get_coverage(filename) coverage_files = coverage['files'] @@ -116,3 +167,6 @@ def explode_path(path): else: return [first] +def get_file_extension(filename): + period_delimited_segments = filename.split(".") + return "" if len(period_delimited_segments) < 2 else period_delimited_segments[-1] From 1c960932d3ae79f660cc10eb94d2e773d1eb48f1 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 17:32:15 +1000 Subject: [PATCH 12/41] Add configurable coverage highlighting levels. --- SublimeRubyCoverage.sublime-settings | 16 +++++++++-- toggle_ruby_coverage.py | 43 +++++++++++++++++++++------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/SublimeRubyCoverage.sublime-settings b/SublimeRubyCoverage.sublime-settings index df6056f..2a11f73 100644 --- a/SublimeRubyCoverage.sublime-settings +++ b/SublimeRubyCoverage.sublime-settings @@ -5,6 +5,16 @@ */ "auto_scoll_to_uncovered": false, + /* + Sets the coverage levels at which coverage color + shades are applied to a line. + */ + "coverage_levels": { + "covered": 1, + "more_covered": 2, + "most_covered": 50 + }, + "colors": { /* Colors used to display coverage. @@ -13,11 +23,11 @@ "uncovered_foreground": "#F9F9F4", "uncovered_background": "#A83732", "covered_foreground": "#F9F9F4", - "covered_background": "#37A832", + "covered_background": "#287020", "covered_foreground_bold": "#F9F9F4", - "covered_background_bold": "#287020", + "covered_background_bold": "#37A832", "covered_foreground_extrabold": "#F9F9F4", - "covered_background_extrabold": "#287020" + "covered_background_extrabold": "#43D53E" } }, diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 11fcda4..09cbbe2 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -35,18 +35,38 @@ def show_coverage(self): if view.window(): sublime.status_message('No coverage data for this file.') return - regions = [] - for line_number, line_coverage in list(enumerate(coverage)): - if line_coverage == 0: - regions.append(view.full_line(view.text_point(line_number, 0))) + coverage_levels = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("coverage_levels") - if regions: - file_ext = get_file_extension(os.path.basename(filename)) - augment_color_scheme(view, file_ext) + uncovered_regions = [] + covered_regions = [] + more_covered_regions = [] + most_covered_regions = [] - view.add_regions('SublimeRubyCoverage', regions, - 'coverage.uncovered') + for line_number, line_coverage in list(enumerate(coverage)): + if line_coverage is None: + continue + if line_coverage >= coverage_levels['most_covered']: + most_covered_regions.append(view.full_line(view.text_point(line_number, 0))) + elif line_coverage >= coverage_levels['more_covered']: + more_covered_regions.append(view.full_line(view.text_point(line_number, 0))) + elif line_coverage >= coverage_levels['covered']: + covered_regions.append(view.full_line(view.text_point(line_number, 0))) + else: + uncovered_regions.append(view.full_line(view.text_point(line_number, 0))) + + file_ext = get_file_extension(os.path.basename(filename)) + augment_color_scheme(view, file_ext) + + view.add_regions('ruby-coverage-uncovered-lines', uncovered_regions, + 'coverage.uncovered') + view.add_regions('ruby-coverage-covered-lines', covered_regions, + 'coverage.covered') + view.add_regions('ruby-coverage-more-covered-lines', more_covered_regions, + 'coverage.covered.more') + view.add_regions('ruby-coverage-most-covered-lines', most_covered_regions, + 'coverage.covered.most') + return True def get_filename(self): view = self.view @@ -59,7 +79,10 @@ def hide_coverage(self): view = self.view restore_color_scheme(view) view.erase_status('SublimeRubyCoverage') - view.erase_regions('SublimeRubyCoverage') + view.erase_regions('ruby-coverage-uncovered-lines') + view.erase_regions('ruby-coverage-covered-lines') + view.erase_regions('ruby-coverage-more-covered-lines') + view.erase_regions('ruby-coverage-most-covered-lines') def file_exempt(self, filename): normalized_filename = os.path.normpath(filename).replace('\\', '/') From a1bd3706e3dcaeafde992e423c9d58e83a36af6a Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 17:32:30 +1000 Subject: [PATCH 13/41] Fix broken color scheme on excluded files. --- toggle_ruby_coverage.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 09cbbe2..83f9118 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -18,8 +18,8 @@ def run(self, edit): self.hide_coverage() settings.erase('ruby_coverage.visible') else: - self.show_coverage() - settings.set('ruby_coverage.visible', True) + if self.show_coverage(): + settings.set('ruby_coverage.visible', True) def show_coverage(self): view = self.view @@ -144,8 +144,9 @@ def augment_color_scheme(view, file_ext): def restore_color_scheme(view): settings = view.settings() original_color_scheme = settings.get("ruby_coverage.original_color_scheme") - settings.set("color_scheme", original_color_scheme) - settings.erase("ruby_coverage.original_color_scheme") + if original_color_scheme: + settings.set("color_scheme", original_color_scheme) + settings.erase("ruby_coverage.original_color_scheme") def get_coverage_for_filename(filename): coverage = get_coverage(filename) @@ -183,13 +184,6 @@ def get_project_root_directory(filename): return get_project_root_directory(parent) -def explode_path(path): - first, second = os.path.split(path) - if second: - return explode_path(first) + [second] - else: - return [first] - def get_file_extension(filename): period_delimited_segments = filename.split(".") return "" if len(period_delimited_segments) < 2 else period_delimited_segments[-1] From a73cc7b1fb3092807dabeebf39c6cec022485e49 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 17:32:47 +1000 Subject: [PATCH 14/41] Display coverage level per line in status bar. --- ruby_coverage_status.py | 118 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 ruby_coverage_status.py diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py new file mode 100644 index 0000000..fd7f80b --- /dev/null +++ b/ruby_coverage_status.py @@ -0,0 +1,118 @@ +import os +import sublime +import sublime_plugin +import json +import re + +STATUS_KEY = 'ruby-coverage-status' + +class RubyCoverageStatusListener(sublime_plugin.EventListener): + """Show coverage statistics in status bar.""" + + def on_selection_modified(self, view): + if 'source.ruby' not in view.scope_name(0): + return + + self.view = view + if sublime.load_settings('SublimeRubyCoverage.sublime-settings').get('coverage_status_in_status_bar'): + sublime.set_timeout_async(self.update_status, 0) + else: + self.erase_status() + + def update_status(self): + view = self.view + view.set_status(STATUS_KEY, self.get_view_coverage_status()) + + def erase_status(self): + view = self.view + view.erase_status(STATUS_KEY) + + def get_view_coverage_status(self): + view = self.view + + filename = self.get_filename() + if not filename: + self.erase_status() + + coverage = get_coverage_for_filename(filename) + if not coverage: + self.erase_status() + + line_number = self.get_line_number() + if line_number is None: + self.erase_status() + + line_coverage = coverage[line_number] + if line_coverage is None: + return 'Line not executable' + + return 'Line covered × {}'.format(line_coverage) if line_coverage > 0 else 'Line NOT COVERED!' + + def get_filename(self): + view = self.view + filename = view.file_name() + if not filename or self.file_exempt(filename): + return + return filename + + def file_exempt(self, filename): + normalized_filename = os.path.normpath(filename).replace('\\', '/') + + exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', + r'\.gemspec'] + + root = get_project_root_directory(self.view.file_name()) + ignore = os.path.join(root, '.covignore') + if os.path.isfile(ignore): + for path in open(ignore).read().rstrip("\n").split("\n"): + exempt.append(path) + + for pattern in exempt: + if re.compile(pattern).search(normalized_filename) is not None: + return True + return False + + def get_line_number(self): + view = self.view + + regions = view.sel() + if len(regions) > 1: + return + + return view.rowcol(regions[0].a)[0] + +def get_coverage_for_filename(filename): + coverage = get_coverage(filename) + coverage_files = coverage['files'] + for coverage_file in coverage_files: + if coverage_file['filename'] == filename: + return coverage_file['coverage'] + +def get_coverage(filename): + filename = get_coverage_filename(filename) + with open(filename) as json_file: + return json.load(json_file) + +def get_coverage_filename(filename): + project_root = get_project_root_directory(filename) + if not project_root: + return + + coverage_filename = os.path.join(project_root, 'coverage', 'coverage.json') + if not os.access(coverage_filename, os.R_OK): + print('Could not find coverage.json file.') + return + + return coverage_filename + +def get_project_root_directory(filename): + """the parent directory that contains a directory called 'coverage'""" + coverage_directory = os.path.join(filename, 'coverage') + if os.access(coverage_directory, os.R_OK): + return filename + + parent, current = os.path.split(filename) + if not current: + print('Could not find coverage directory.') + + return get_project_root_directory(parent) From 2ab2ce25f6ef3c33d81800071ef2e036b908ed9a Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 17:59:16 +1000 Subject: [PATCH 15/41] Scroll to first uncovered line when showing coverage view (off by default). --- toggle_ruby_coverage.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 83f9118..c75f103 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -20,6 +20,8 @@ def run(self, edit): else: if self.show_coverage(): settings.set('ruby_coverage.visible', True) + if self.is_auto_scroll_enabled(): + self.scroll_to_uncovered() def show_coverage(self): view = self.view @@ -101,6 +103,24 @@ def file_exempt(self, filename): return True return False + def is_auto_scroll_enabled(self): + settings = sublime.load_settings("SublimeRubyCoverage.sublime-settings") + return settings.get("auto_scoll_to_uncovered", False) + + def scroll_to_uncovered(self): + view = self.view + regions = view.sel() + if len(regions) > 1 or regions[0].size() > 0: + return + + uncovered_lines = view.get_regions('ruby-coverage-uncovered-lines') + if uncovered_lines and len(uncovered_lines) > 0: + first_uncovered = uncovered_lines[0].a + + view.sel().clear() + view.sel().add(sublime.Region(first_uncovered, first_uncovered)) + view.show_at_center(first_uncovered) + def augment_color_scheme(view, file_ext): """ Given a target view, generate a new color scheme from the original with From ed0fefe6ea65e7849f050e26e82bfd90bd1bca58 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 18:01:46 +1000 Subject: [PATCH 16/41] Add package settings menu items. --- Main.sublime-menu | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 Main.sublime-menu diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..7780587 --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,29 @@ +[ + { + "id": "preferences", + "children": [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": [ + { + "caption": "SublimeRubyCoverage", + "children": [ + { + "command": "open_file", + "args": {"file": "${packages}/SublimeRubyCoverage/SublimeRubyCoverage.sublime-settings"}, + "caption": "Settings – Default" + }, { + "command": "open_file", + "args": {"file": "${packages}/User/SublimeRubyCoverage.sublime-settings"}, + "caption": "Settings – User" + } + ] + } + ] + } + ] + } + +] From 028fdce025d604c04fb961dcc5770e5fe328284c Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 18:21:01 +1000 Subject: [PATCH 17/41] Display file coverage percentage in status bar. --- ruby_coverage_status.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index fd7f80b..b264b40 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -42,11 +42,21 @@ def get_view_coverage_status(self): if line_number is None: self.erase_status() - line_coverage = coverage[line_number] + file_coverage = "File covered {:.1f}% ({}/{})".format( + coverage['covered_percent'], + coverage['covered_lines'], + coverage['lines_of_code'] + ) + + line_coverage = coverage['coverage'][line_number] if line_coverage is None: - return 'Line not executable' + line_coverage = 'Line not executable' + elif line_coverage > 0: + line_coverage = 'Line covered × {}'.format(line_coverage) + else: + line_coverage = 'Line not covered' - return 'Line covered × {}'.format(line_coverage) if line_coverage > 0 else 'Line NOT COVERED!' + return file_coverage + ', ' + line_coverage def get_filename(self): view = self.view @@ -86,7 +96,7 @@ def get_coverage_for_filename(filename): coverage_files = coverage['files'] for coverage_file in coverage_files: if coverage_file['filename'] == filename: - return coverage_file['coverage'] + return coverage_file def get_coverage(filename): filename = get_coverage_filename(filename) From 579fa90999b7067da732d7dc5a23c0dc27efde72 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 18:48:40 +1000 Subject: [PATCH 18/41] Combine adjacent lines with same coverage level into one region. --- toggle_ruby_coverage.py | 44 +++++++++++++++++++++++++++++++++++------ 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index c75f103..116f713 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -45,17 +45,49 @@ def show_coverage(self): more_covered_regions = [] most_covered_regions = [] + current_coverage_regions = None + current_region_start = None + current_region_end = None + for line_number, line_coverage in list(enumerate(coverage)): if line_coverage is None: - continue - if line_coverage >= coverage_levels['most_covered']: - most_covered_regions.append(view.full_line(view.text_point(line_number, 0))) + # first line after different coverage data + if current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) + current_coverage_regions = None + elif line_coverage >= coverage_levels['most_covered']: + # first line after different coverage data + if current_coverage_regions is not most_covered_regions: + if current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) + current_region_start = view.text_point(line_number, 0) + current_coverage_regions = most_covered_regions elif line_coverage >= coverage_levels['more_covered']: - more_covered_regions.append(view.full_line(view.text_point(line_number, 0))) + # first line after different coverage data + if current_coverage_regions is not more_covered_regions: + if current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) + current_region_start = view.text_point(line_number, 0) + current_coverage_regions = more_covered_regions elif line_coverage >= coverage_levels['covered']: - covered_regions.append(view.full_line(view.text_point(line_number, 0))) + # first line after different coverage data + if current_coverage_regions is not covered_regions: + if current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) + current_region_start = view.text_point(line_number, 0) + current_coverage_regions = covered_regions else: - uncovered_regions.append(view.full_line(view.text_point(line_number, 0))) + # first line after different coverage data + if current_coverage_regions is not uncovered_regions: + if current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) + current_region_start = view.text_point(line_number, 0) + current_coverage_regions = uncovered_regions file_ext = get_file_extension(os.path.basename(filename)) augment_color_scheme(view, file_ext) From 3eb0b813cf3b3fb5e46d26e9b4d75b73d8ad8ee8 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 23:24:25 +1000 Subject: [PATCH 19/41] Extract shared functionality into JsonCoverageReader. --- common/json_coverage_reader.py | 67 ++++++++++++++++++++++++++++++++ ruby_coverage_status.py | 69 +++------------------------------ toggle_ruby_coverage.py | 70 +++------------------------------- 3 files changed, 79 insertions(+), 127 deletions(-) create mode 100644 common/json_coverage_reader.py diff --git a/common/json_coverage_reader.py b/common/json_coverage_reader.py new file mode 100644 index 0000000..5b635bf --- /dev/null +++ b/common/json_coverage_reader.py @@ -0,0 +1,67 @@ +import os +import json +import re + +class JsonCoverageReader: + """ + For any file in a project with JSON SimpleCov coverage data, + makes whole-file and line-specific coverage data available. + """ + + def __init__(self, filename): + """ Load coverage data given the filename for any file in the project. """ + self.project_root = get_project_root(filename) + self.coverage = self.get_coverage_data() + + def get_file_coverage(self, filename): + if self.is_file_exempt(filename): + return + + coverage_files = self.coverage['files'] + for coverage_file in coverage_files: + if coverage_file['filename'] == filename: + return coverage_file + + def is_file_exempt(self, filename): + normalized_filename = os.path.normpath(filename).replace('\\', '/') + + exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', + r'\.gemspec'] + + ignore = os.path.join(self.project_root, '.covignore') + if os.path.isfile(ignore): + for path in open(ignore).read().rstrip("\n").split("\n"): + exempt.append(path) + + for pattern in exempt: + if re.compile(pattern).search(normalized_filename) is not None: + return True + return False + + def get_coverage_data(self): + coverage_filename = self.get_coverage_filename() + with open(coverage_filename) as json_file: + return json.load(json_file) + + def get_coverage_filename(self): + if not self.project_root: + return + + coverage_filename = os.path.join(self.project_root, 'coverage', 'coverage.json') + if not os.access(coverage_filename, os.R_OK): + print('Could not find coverage.json file.') + return + + return coverage_filename + +def get_project_root(filename): + """the parent directory that contains a directory called 'coverage'""" + coverage_directory = os.path.join(filename, 'coverage') + if os.access(coverage_directory, os.R_OK): + return filename + + parent, current = os.path.split(filename) + if not current: + print('Could not find coverage directory.') + + return get_project_root(parent) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index b264b40..218e464 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -4,6 +4,8 @@ import json import re +from .common.json_coverage_reader import JsonCoverageReader + STATUS_KEY = 'ruby-coverage-status' class RubyCoverageStatusListener(sublime_plugin.EventListener): @@ -30,12 +32,13 @@ def erase_status(self): def get_view_coverage_status(self): view = self.view - filename = self.get_filename() + filename = view.file_name() if not filename: self.erase_status() - coverage = get_coverage_for_filename(filename) - if not coverage: + r = JsonCoverageReader(filename) + coverage = r.get_file_coverage(filename) if r else None + if coverage is None: self.erase_status() line_number = self.get_line_number() @@ -58,30 +61,6 @@ def get_view_coverage_status(self): return file_coverage + ', ' + line_coverage - def get_filename(self): - view = self.view - filename = view.file_name() - if not filename or self.file_exempt(filename): - return - return filename - - def file_exempt(self, filename): - normalized_filename = os.path.normpath(filename).replace('\\', '/') - - exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', - r'\.gemspec'] - - root = get_project_root_directory(self.view.file_name()) - ignore = os.path.join(root, '.covignore') - if os.path.isfile(ignore): - for path in open(ignore).read().rstrip("\n").split("\n"): - exempt.append(path) - - for pattern in exempt: - if re.compile(pattern).search(normalized_filename) is not None: - return True - return False - def get_line_number(self): view = self.view @@ -90,39 +69,3 @@ def get_line_number(self): return return view.rowcol(regions[0].a)[0] - -def get_coverage_for_filename(filename): - coverage = get_coverage(filename) - coverage_files = coverage['files'] - for coverage_file in coverage_files: - if coverage_file['filename'] == filename: - return coverage_file - -def get_coverage(filename): - filename = get_coverage_filename(filename) - with open(filename) as json_file: - return json.load(json_file) - -def get_coverage_filename(filename): - project_root = get_project_root_directory(filename) - if not project_root: - return - - coverage_filename = os.path.join(project_root, 'coverage', 'coverage.json') - if not os.access(coverage_filename, os.R_OK): - print('Could not find coverage.json file.') - return - - return coverage_filename - -def get_project_root_directory(filename): - """the parent directory that contains a directory called 'coverage'""" - coverage_directory = os.path.join(filename, 'coverage') - if os.access(coverage_directory, os.R_OK): - return filename - - parent, current = os.path.split(filename) - if not current: - print('Could not find coverage directory.') - - return get_project_root_directory(parent) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 116f713..ae19b5c 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -5,6 +5,7 @@ import re from .theme_generator import ThemeGenerator +from .common.json_coverage_reader import JsonCoverageReader class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): """Show/hide coverage of current file based on a previous coverage run.""" @@ -26,12 +27,13 @@ def run(self, edit): def show_coverage(self): view = self.view - filename = self.get_filename() + filename = view.file_name() if not filename: return - coverage = get_coverage_for_filename(filename) - if not coverage: + r = JsonCoverageReader(filename) + coverage = r.get_file_coverage(filename) if r else None + if coverage is None: regions.append(sublime.Region(0,view.size())) view.set_status('SublimeRubyCoverage', 'NOT COVERED') if view.window(): @@ -49,7 +51,7 @@ def show_coverage(self): current_region_start = None current_region_end = None - for line_number, line_coverage in list(enumerate(coverage)): + for line_number, line_coverage in list(enumerate(coverage['coverage'])): if line_coverage is None: # first line after different coverage data if current_coverage_regions is not None: @@ -102,13 +104,6 @@ def show_coverage(self): 'coverage.covered.most') return True - def get_filename(self): - view = self.view - filename = view.file_name() - if not filename or self.file_exempt(filename): - return - return filename - def hide_coverage(self): view = self.view restore_color_scheme(view) @@ -118,23 +113,6 @@ def hide_coverage(self): view.erase_regions('ruby-coverage-more-covered-lines') view.erase_regions('ruby-coverage-most-covered-lines') - def file_exempt(self, filename): - normalized_filename = os.path.normpath(filename).replace('\\', '/') - - exempt = [r'/test/', r'/spec/', r'/features/', r'Gemfile$', r'Rakefile$', r'\.rake$', - r'\.gemspec'] - - root = get_project_root_directory(self.view.file_name()) - ignore = os.path.join(root, '.covignore') - if os.path.isfile(ignore): - for path in open(ignore).read().rstrip("\n").split("\n"): - exempt.append(path) - - for pattern in exempt: - if re.compile(pattern).search(normalized_filename) is not None: - return True - return False - def is_auto_scroll_enabled(self): settings = sublime.load_settings("SublimeRubyCoverage.sublime-settings") return settings.get("auto_scoll_to_uncovered", False) @@ -200,42 +178,6 @@ def restore_color_scheme(view): settings.set("color_scheme", original_color_scheme) settings.erase("ruby_coverage.original_color_scheme") -def get_coverage_for_filename(filename): - coverage = get_coverage(filename) - coverage_files = coverage['files'] - for coverage_file in coverage_files: - if coverage_file['filename'] == filename: - return coverage_file['coverage'] - -def get_coverage(filename): - filename = get_coverage_filename(filename) - with open(filename) as json_file: - return json.load(json_file) - -def get_coverage_filename(filename): - project_root = get_project_root_directory(filename) - if not project_root: - return - - coverage_filename = os.path.join(project_root, 'coverage', 'coverage.json') - if not os.access(coverage_filename, os.R_OK): - print('Could not find coverage.json file.') - return - - return coverage_filename - -def get_project_root_directory(filename): - """the parent directory that contains a directory called 'coverage'""" - coverage_directory = os.path.join(filename, 'coverage') - if os.access(coverage_directory, os.R_OK): - return filename - - parent, current = os.path.split(filename) - if not current: - print('Could not find coverage directory.') - - return get_project_root_directory(parent) - def get_file_extension(filename): period_delimited_segments = filename.split(".") return "" if len(period_delimited_segments) < 2 else period_delimited_segments[-1] From cdfc37cf5615bd2b3a51bdae147d7127e61b4534 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 23:35:06 +1000 Subject: [PATCH 20/41] Move ThemeGenerator to common directory. --- theme_generator.py => common/theme_generator.py | 0 toggle_ruby_coverage.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename theme_generator.py => common/theme_generator.py (100%) diff --git a/theme_generator.py b/common/theme_generator.py similarity index 100% rename from theme_generator.py rename to common/theme_generator.py diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index ae19b5c..edf65b3 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -4,7 +4,7 @@ import json import re -from .theme_generator import ThemeGenerator +from .common.theme_generator import ThemeGenerator from .common.json_coverage_reader import JsonCoverageReader class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): From 7293e9ed4fffa94f3f4067e3bdc3ea59e984581f Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 23:35:37 +1000 Subject: [PATCH 21/41] Fix error on generating status for non-covered file. --- ruby_coverage_status.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index 218e464..29bcbd1 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -40,6 +40,7 @@ def get_view_coverage_status(self): coverage = r.get_file_coverage(filename) if r else None if coverage is None: self.erase_status() + return '' line_number = self.get_line_number() if line_number is None: From bdc7d546d8d781b39d35647e86ca3ebb27050909 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 23:37:51 +1000 Subject: [PATCH 22/41] Display coverage status immediately on file open. --- ruby_coverage_status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index 29bcbd1..2a170d0 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -11,6 +11,9 @@ class RubyCoverageStatusListener(sublime_plugin.EventListener): """Show coverage statistics in status bar.""" + def on_load(self, view): + self.on_selection_modified(view) + def on_selection_modified(self, view): if 'source.ruby' not in view.scope_name(0): return From b7eeb94916b60a58b3d25940804446619bd07b27 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sat, 13 Jun 2015 23:46:04 +1000 Subject: [PATCH 23/41] Refactor: extract methods. --- toggle_ruby_coverage.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index edf65b3..36bfac0 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -19,20 +19,26 @@ def run(self, edit): self.hide_coverage() settings.erase('ruby_coverage.visible') else: - if self.show_coverage(): + filename = self.get_filename() + coverage = self.get_coverage(filename) + if self.show_coverage(filename, coverage): settings.set('ruby_coverage.visible', True) if self.is_auto_scroll_enabled(): self.scroll_to_uncovered() - def show_coverage(self): - view = self.view + def get_filename(self): + return self.view.file_name() - filename = view.file_name() - if not filename: - return + def get_coverage(self, filename): + view = self.view r = JsonCoverageReader(filename) coverage = r.get_file_coverage(filename) if r else None + return coverage + + def show_coverage(self, filename, coverage): + view = self.view + if coverage is None: regions.append(sublime.Region(0,view.size())) view.set_status('SublimeRubyCoverage', 'NOT COVERED') From dc333069f4536fdc978d76924b098d10a1a8a50a Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 00:01:31 +1000 Subject: [PATCH 24/41] Fix error on status for last line in file. --- ruby_coverage_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index 2a170d0..0850ed0 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -55,7 +55,7 @@ def get_view_coverage_status(self): coverage['lines_of_code'] ) - line_coverage = coverage['coverage'][line_number] + line_coverage = coverage['coverage'][line_number] if len(coverage['coverage']) > line_number else None if line_coverage is None: line_coverage = 'Line not executable' elif line_coverage > 0: From b38934cb954d64cb60398b90133a895d18f04cad Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 00:02:06 +1000 Subject: [PATCH 25/41] DRY up coverage region chunking. --- toggle_ruby_coverage.py | 70 +++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 42 deletions(-) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 36bfac0..eadb60c 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -40,62 +40,29 @@ def show_coverage(self, filename, coverage): view = self.view if coverage is None: - regions.append(sublime.Region(0,view.size())) - view.set_status('SublimeRubyCoverage', 'NOT COVERED') - if view.window(): - sublime.status_message('No coverage data for this file.') + self.show_no_coverage() return - coverage_levels = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("coverage_levels") - uncovered_regions = [] covered_regions = [] more_covered_regions = [] most_covered_regions = [] + coverage_levels = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("coverage_levels") current_coverage_regions = None - current_region_start = None - current_region_end = None - + self.reset_coverage_lines() for line_number, line_coverage in list(enumerate(coverage['coverage'])): if line_coverage is None: - # first line after different coverage data - if current_coverage_regions is not None: - current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() - current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) - current_coverage_regions = None + self.add_coverage_line(line_number, None) elif line_coverage >= coverage_levels['most_covered']: - # first line after different coverage data - if current_coverage_regions is not most_covered_regions: - if current_coverage_regions is not None: - current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() - current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) - current_region_start = view.text_point(line_number, 0) - current_coverage_regions = most_covered_regions + self.add_coverage_line(line_number, most_covered_regions) elif line_coverage >= coverage_levels['more_covered']: - # first line after different coverage data - if current_coverage_regions is not more_covered_regions: - if current_coverage_regions is not None: - current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() - current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) - current_region_start = view.text_point(line_number, 0) - current_coverage_regions = more_covered_regions + self.add_coverage_line(line_number, more_covered_regions) elif line_coverage >= coverage_levels['covered']: - # first line after different coverage data - if current_coverage_regions is not covered_regions: - if current_coverage_regions is not None: - current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() - current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) - current_region_start = view.text_point(line_number, 0) - current_coverage_regions = covered_regions + self.add_coverage_line(line_number, covered_regions) else: - # first line after different coverage data - if current_coverage_regions is not uncovered_regions: - if current_coverage_regions is not None: - current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() - current_coverage_regions.append(sublime.Region(current_region_start, current_region_end)) - current_region_start = view.text_point(line_number, 0) - current_coverage_regions = uncovered_regions + self.add_coverage_line(line_number, uncovered_regions) + self.add_coverage_line(line_number + 1, None) file_ext = get_file_extension(os.path.basename(filename)) augment_color_scheme(view, file_ext) @@ -110,6 +77,25 @@ def show_coverage(self, filename, coverage): 'coverage.covered.most') return True + def reset_coverage_lines(self): + self.current_coverage_regions = None + self.current_region_start = None + + def add_coverage_line(self, line_number, line_coverage_regions): + view = self.view + if self.current_coverage_regions is not line_coverage_regions: + if self.current_coverage_regions is not None: + current_region_end = view.full_line(view.text_point(line_number - 1, 0)).end() + self.current_coverage_regions.append(sublime.Region(self.current_region_start, current_region_end)) + self.current_region_start = view.text_point(line_number, 0) + self.current_coverage_regions = line_coverage_regions + + def show_no_coverage(self): + regions.append(sublime.Region(0,view.size())) + view.set_status('SublimeRubyCoverage', 'NOT COVERED') + if view.window(): + sublime.status_message('No coverage data for this file.') + def hide_coverage(self): view = self.view restore_color_scheme(view) From e0808ce54101168a7d2370eadfaa2952f0fd050d Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 00:15:58 +1000 Subject: [PATCH 26/41] Better support uncovered files. --- ruby_coverage_status.py | 3 +-- toggle_ruby_coverage.py | 21 +++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index 0850ed0..6625262 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -42,8 +42,7 @@ def get_view_coverage_status(self): r = JsonCoverageReader(filename) coverage = r.get_file_coverage(filename) if r else None if coverage is None: - self.erase_status() - return '' + return 'File not covered' line_number = self.get_line_number() if line_number is None: diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index eadb60c..416e961 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -21,10 +21,10 @@ def run(self, edit): else: filename = self.get_filename() coverage = self.get_coverage(filename) - if self.show_coverage(filename, coverage): - settings.set('ruby_coverage.visible', True) - if self.is_auto_scroll_enabled(): - self.scroll_to_uncovered() + self.show_coverage(filename, coverage) + settings.set('ruby_coverage.visible', True) + if self.is_auto_scroll_enabled(): + self.scroll_to_uncovered() def get_filename(self): return self.view.file_name() @@ -39,6 +39,9 @@ def get_coverage(self, filename): def show_coverage(self, filename, coverage): view = self.view + file_ext = get_file_extension(os.path.basename(filename)) + augment_color_scheme(view, file_ext) + if coverage is None: self.show_no_coverage() return @@ -64,9 +67,6 @@ def show_coverage(self, filename, coverage): self.add_coverage_line(line_number, uncovered_regions) self.add_coverage_line(line_number + 1, None) - file_ext = get_file_extension(os.path.basename(filename)) - augment_color_scheme(view, file_ext) - view.add_regions('ruby-coverage-uncovered-lines', uncovered_regions, 'coverage.uncovered') view.add_regions('ruby-coverage-covered-lines', covered_regions, @@ -75,7 +75,6 @@ def show_coverage(self, filename, coverage): 'coverage.covered.more') view.add_regions('ruby-coverage-most-covered-lines', most_covered_regions, 'coverage.covered.most') - return True def reset_coverage_lines(self): self.current_coverage_regions = None @@ -91,8 +90,10 @@ def add_coverage_line(self, line_number, line_coverage_regions): self.current_coverage_regions = line_coverage_regions def show_no_coverage(self): - regions.append(sublime.Region(0,view.size())) - view.set_status('SublimeRubyCoverage', 'NOT COVERED') + view = self.view + view.add_regions('ruby-coverage-uncovered-lines', + [sublime.Region(0, view.size())], + 'coverage.uncovered') if view.window(): sublime.status_message('No coverage data for this file.') From 0507a2077fb821ccae8e013941d4fc1ec2f72b27 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 00:25:59 +1000 Subject: [PATCH 27/41] Convert some functions into class methods. --- toggle_ruby_coverage.py | 108 ++++++++++++++++++++-------------------- 1 file changed, 55 insertions(+), 53 deletions(-) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 416e961..2200e4c 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -39,8 +39,7 @@ def get_coverage(self, filename): def show_coverage(self, filename, coverage): view = self.view - file_ext = get_file_extension(os.path.basename(filename)) - augment_color_scheme(view, file_ext) + self.augment_color_scheme() if coverage is None: self.show_no_coverage() @@ -99,7 +98,7 @@ def show_no_coverage(self): def hide_coverage(self): view = self.view - restore_color_scheme(view) + self.restore_color_scheme() view.erase_status('SublimeRubyCoverage') view.erase_regions('ruby-coverage-uncovered-lines') view.erase_regions('ruby-coverage-covered-lines') @@ -124,53 +123,56 @@ def scroll_to_uncovered(self): view.sel().add(sublime.Region(first_uncovered, first_uncovered)) view.show_at_center(first_uncovered) -def augment_color_scheme(view, file_ext): - """ - Given a target view, generate a new color scheme from the original with - additional coverage-related style rules added. Save this color scheme to - disk and set it as the target view's active color scheme. - - (Hat tip to GitSavvy for this technique!) - """ - colors = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("colors") - - settings = view.settings() - original_color_scheme = settings.get("color_scheme") - settings.set("ruby_coverage.original_color_scheme", original_color_scheme) - themeGenerator = ThemeGenerator(original_color_scheme) - themeGenerator.add_scoped_style( - "SublimeRubyCoverage Uncovered Line", - "coverage.uncovered", - background=colors["coverage"]["uncovered_background"], - foreground=colors["coverage"]["uncovered_foreground"] - ) - themeGenerator.add_scoped_style( - "SublimeRubyCoverage Covered Line", - "coverage.covered", - background=colors["coverage"]["covered_background"], - foreground=colors["coverage"]["covered_foreground"] - ) - themeGenerator.add_scoped_style( - "SublimeRubyCoverage More Covered Line", - "coverage.covered.more", - background=colors["coverage"]["covered_background_bold"], - foreground=colors["coverage"]["covered_foreground_bold"] - ) - themeGenerator.add_scoped_style( - "SublimeRubyCoverage Most Covered Line", - "coverage.covered.most", - background=colors["coverage"]["covered_background_extrabold"], - foreground=colors["coverage"]["covered_foreground_extrabold"] - ) - themeGenerator.apply_new_theme("ruby-coverage-view." + file_ext, view) - -def restore_color_scheme(view): - settings = view.settings() - original_color_scheme = settings.get("ruby_coverage.original_color_scheme") - if original_color_scheme: - settings.set("color_scheme", original_color_scheme) - settings.erase("ruby_coverage.original_color_scheme") - -def get_file_extension(filename): - period_delimited_segments = filename.split(".") - return "" if len(period_delimited_segments) < 2 else period_delimited_segments[-1] + def augment_color_scheme(self): + """ + Generate a new color scheme from the original with additional coverage- + related style rules added. Save this color scheme to disk and set it as + the target view's active color scheme. + + (Hat tip to GitSavvy for this technique!) + """ + view = self.view + colors = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("colors") + file_ext = self.get_filename_ext() + + settings = view.settings() + original_color_scheme = settings.get("color_scheme") + settings.set("ruby_coverage.original_color_scheme", original_color_scheme) + themeGenerator = ThemeGenerator(original_color_scheme) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Uncovered Line", + "coverage.uncovered", + background = colors["coverage"]["uncovered_background"], + foreground = colors["coverage"]["uncovered_foreground"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Covered Line", + "coverage.covered", + background = colors["coverage"]["covered_background"], + foreground = colors["coverage"]["covered_foreground"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage More Covered Line", + "coverage.covered.more", + background = colors["coverage"]["covered_background_bold"], + foreground = colors["coverage"]["covered_foreground_bold"] + ) + themeGenerator.add_scoped_style( + "SublimeRubyCoverage Most Covered Line", + "coverage.covered.most", + background = colors["coverage"]["covered_background_extrabold"], + foreground = colors["coverage"]["covered_foreground_extrabold"] + ) + themeGenerator.apply_new_theme("ruby-coverage-view." + file_ext, view) + + def get_filename_ext(self): + filename = os.path.basename(self.get_filename()) + period_delimited_segments = filename.split(".") + return '' if len(period_delimited_segments) < 2 else period_delimited_segments[-1] + + def restore_color_scheme(self): + settings = self.view.settings() + original_color_scheme = settings.get("ruby_coverage.original_color_scheme") + if original_color_scheme: + settings.set("color_scheme", original_color_scheme) + settings.erase("ruby_coverage.original_color_scheme") From fa64cd3e1064f968fe0e9083ee27153f4feda5cd Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 09:47:52 +1000 Subject: [PATCH 28/41] Fix errors editing Ruby files with no coverage data. --- common/json_coverage_reader.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/common/json_coverage_reader.py b/common/json_coverage_reader.py index 5b635bf..f496784 100644 --- a/common/json_coverage_reader.py +++ b/common/json_coverage_reader.py @@ -11,10 +11,10 @@ class JsonCoverageReader: def __init__(self, filename): """ Load coverage data given the filename for any file in the project. """ self.project_root = get_project_root(filename) - self.coverage = self.get_coverage_data() + self.coverage = self.get_coverage_data() if self.project_root else None def get_file_coverage(self, filename): - if self.is_file_exempt(filename): + if self.coverage is None or self.is_file_exempt(filename): return coverage_files = self.coverage['files'] @@ -40,8 +40,10 @@ def is_file_exempt(self, filename): def get_coverage_data(self): coverage_filename = self.get_coverage_filename() - with open(coverage_filename) as json_file: - return json.load(json_file) + if not coverage_filename: + return + + return json.load(open(coverage_filename)) def get_coverage_filename(self): if not self.project_root: @@ -63,5 +65,6 @@ def get_project_root(filename): parent, current = os.path.split(filename) if not current: print('Could not find coverage directory.') + return return get_project_root(parent) From 06acc6dcd6e6176fb34a08dc8d4f6edcf39b6598 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 15:33:29 +1000 Subject: [PATCH 29/41] Add Show Project Ruby Coverage command. --- SublimeRubyCoverage.sublime-commands | 3 +- common/json_coverage_reader.py | 12 +++++++- show_project_ruby_coverage.py | 44 ++++++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 show_project_ruby_coverage.py diff --git a/SublimeRubyCoverage.sublime-commands b/SublimeRubyCoverage.sublime-commands index 3f1f3fc..9caaac5 100644 --- a/SublimeRubyCoverage.sublime-commands +++ b/SublimeRubyCoverage.sublime-commands @@ -1,3 +1,4 @@ [ - { "caption": "View: Toggle Ruby Coverage", "command": "toggle_ruby_coverage"} + { "caption": "View: Toggle Ruby Coverage", "command": "toggle_ruby_coverage"}, + { "caption": "Show Project Ruby Coverage", "command": "show_project_ruby_coverage"} ] diff --git a/common/json_coverage_reader.py b/common/json_coverage_reader.py index f496784..592e529 100644 --- a/common/json_coverage_reader.py +++ b/common/json_coverage_reader.py @@ -5,7 +5,7 @@ class JsonCoverageReader: """ For any file in a project with JSON SimpleCov coverage data, - makes whole-file and line-specific coverage data available. + makes whole-project, whole-file and line-specific coverage data available. """ def __init__(self, filename): @@ -13,6 +13,12 @@ def __init__(self, filename): self.project_root = get_project_root(filename) self.coverage = self.get_coverage_data() if self.project_root else None + def get_project_coverage(self): + coverage_data = dict(self.coverage) + coverage_data['files'] = list(map(self.make_filename_relative, coverage_data['files'])) + coverage_data['files'].sort(key=lambda file: file['covered_percent']) + return coverage_data + def get_file_coverage(self, filename): if self.coverage is None or self.is_file_exempt(filename): return @@ -45,6 +51,10 @@ def get_coverage_data(self): return json.load(open(coverage_filename)) + def make_filename_relative(self, file): + file['filename'] = os.path.relpath(file['filename'], self.project_root) + return file + def get_coverage_filename(self): if not self.project_root: return diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py new file mode 100644 index 0000000..81f1572 --- /dev/null +++ b/show_project_ruby_coverage.py @@ -0,0 +1,44 @@ +import os +from sublime import Region +from sublime_plugin import TextCommand + +from .common.json_coverage_reader import JsonCoverageReader + +PANEL_NAME = 'ruby-coverage-project' + +class ShowProjectRubyCoverage(TextCommand): + """Show coverage of all files in current file's project in a panel.""" + + def run(self, edit): + self.get_project_coverage() + self.create_output_panel() + self.display_project_coverage(edit) + + def get_project_coverage(self): + filename = self.view.file_name() + + r = JsonCoverageReader(filename) + self.coverage = r.get_project_coverage() if r else None + + def create_output_panel(self): + self.panel = self.view.window().create_output_panel(PANEL_NAME) + + def display_project_coverage(self, edit): + panel = self.panel + panel.set_read_only(False) + panel.erase(edit, Region(0, panel.size())) + panel.insert(edit, 0, self.format_project_coverage()) + panel.set_read_only(True) + panel.show(0) + + self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) + + def format_project_coverage(self): + coverage = self.coverage + + output = '' + + for file in coverage['files']: + output += '{} {:.1f}% covered\n'.format(file['filename'], file['covered_percent']) + + return output From 2a87f09d9300c9372abdca38f3d310135512d612 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 15:33:38 +1000 Subject: [PATCH 30/41] Remove obsolete imports. --- ruby_coverage_status.py | 2 -- toggle_ruby_coverage.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/ruby_coverage_status.py b/ruby_coverage_status.py index 6625262..7cd7892 100644 --- a/ruby_coverage_status.py +++ b/ruby_coverage_status.py @@ -1,8 +1,6 @@ import os import sublime import sublime_plugin -import json -import re from .common.json_coverage_reader import JsonCoverageReader diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 2200e4c..56c22bf 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -1,8 +1,6 @@ import os import sublime import sublime_plugin -import json -import re from .common.theme_generator import ThemeGenerator from .common.json_coverage_reader import JsonCoverageReader From 7a8cdbee9bc637c1bd74fe88ee4d9ba409e7ae89 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 15:51:21 +1000 Subject: [PATCH 31/41] Right-justify coverage values in Project Coverage view. --- show_project_ruby_coverage.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index 81f1572..3d4e25e 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -34,11 +34,13 @@ def display_project_coverage(self, edit): self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) def format_project_coverage(self): - coverage = self.coverage + files = self.coverage['files'] output = '' - for file in coverage['files']: - output += '{} {:.1f}% covered\n'.format(file['filename'], file['covered_percent']) + max_filename_length = len(max(files, key=lambda file: len(file['filename']))['filename']) + + for file in files: + output += file['filename'].ljust(max_filename_length) + ' {:>5.1f}% covered\n'.format(file['covered_percent']) return output From 0ee306dfc8b676cfe728194882cfa84b721e1005 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 15:51:35 +1000 Subject: [PATCH 32/41] Tweak command names. --- SublimeRubyCoverage.sublime-commands | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SublimeRubyCoverage.sublime-commands b/SublimeRubyCoverage.sublime-commands index 9caaac5..2c3d02a 100644 --- a/SublimeRubyCoverage.sublime-commands +++ b/SublimeRubyCoverage.sublime-commands @@ -1,4 +1,4 @@ [ - { "caption": "View: Toggle Ruby Coverage", "command": "toggle_ruby_coverage"}, - { "caption": "Show Project Ruby Coverage", "command": "show_project_ruby_coverage"} + { "caption": "RubyCoverage: Toggle Coverage Highlight", "command": "toggle_ruby_coverage"}, + { "caption": "RubyCoverage: Show Project Coverage", "command": "show_project_ruby_coverage"} ] From 85ebdf35f2949d9bde40d2bf8cc1733d5aa2a996 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 17:37:07 +1000 Subject: [PATCH 33/41] Add bar graph to project coverage view. --- show_project_ruby_coverage.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index 3d4e25e..53a8be2 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -34,13 +34,23 @@ def display_project_coverage(self, edit): self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) def format_project_coverage(self): + panel = self.panel files = self.coverage['files'] output = '' + viewport_width = int(panel.viewport_extent()[0] / panel.em_width()) max_filename_length = len(max(files, key=lambda file: len(file['filename']))['filename']) + coverage_length = len('100%') + graph_width = viewport_width - max_filename_length - coverage_length - 4 for file in files: - output += file['filename'].ljust(max_filename_length) + ' {:>5.1f}% covered\n'.format(file['covered_percent']) + graph_bar_width = int(file['covered_percent'] / 100.0 * graph_width) + + filename = file['filename'].ljust(max_filename_length) + graph = ''.ljust(graph_bar_width, '█') + ''.ljust(graph_width - graph_bar_width) + coverage = '{:>5.1f}%'.format(file['covered_percent']) + + output += '{} {} {}\n'.format(filename, graph, coverage) return output From 82e7568b008ff6563fa51fac5326ac9b7ad08a9d Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Sun, 14 Jun 2015 21:47:39 +1000 Subject: [PATCH 34/41] Display project coverage with color-coded bar graph. --- SublimeRubyCoverage.sublime-settings | 17 ++++ show_project_ruby_coverage.py | 127 +++++++++++++++++++++++++-- 2 files changed, 136 insertions(+), 8 deletions(-) diff --git a/SublimeRubyCoverage.sublime-settings b/SublimeRubyCoverage.sublime-settings index 2a11f73..b9ff52a 100644 --- a/SublimeRubyCoverage.sublime-settings +++ b/SublimeRubyCoverage.sublime-settings @@ -28,6 +28,23 @@ "covered_background_bold": "#37A832", "covered_foreground_extrabold": "#F9F9F4", "covered_background_extrabold": "#43D53E" + }, + + /* + Colors used in coverage bar graph. + */ + "graph": { + "0": "#FF0000", + "10": "#FC1807", + "20": "#FC2F07", + "30": "#FC4E07", + "40": "#FEC309", + "50": "#FFFF0A", + "60": "#C3FF09", + "70": "#8CFF08", + "80": "#5BFF07", + "90": "#35FF06", + "100": "#22FF06" } }, diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index 53a8be2..a14655b 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -1,8 +1,9 @@ import os -from sublime import Region +import sublime from sublime_plugin import TextCommand from .common.json_coverage_reader import JsonCoverageReader +from .common.theme_generator import ThemeGenerator PANEL_NAME = 'ruby-coverage-project' @@ -24,13 +25,18 @@ def create_output_panel(self): self.panel = self.view.window().create_output_panel(PANEL_NAME) def display_project_coverage(self, edit): + output, regions = self.format_project_coverage() + panel = self.panel panel.set_read_only(False) - panel.erase(edit, Region(0, panel.size())) - panel.insert(edit, 0, self.format_project_coverage()) + panel.erase(edit, sublime.Region(0, panel.size())) + panel.insert(edit, 0, output) panel.set_read_only(True) panel.show(0) + self.augment_color_scheme() + self.apply_regions(regions) + self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) def format_project_coverage(self): @@ -42,15 +48,120 @@ def format_project_coverage(self): viewport_width = int(panel.viewport_extent()[0] / panel.em_width()) max_filename_length = len(max(files, key=lambda file: len(file['filename']))['filename']) coverage_length = len('100%') - graph_width = viewport_width - max_filename_length - coverage_length - 4 + graph_width = viewport_width - max_filename_length - coverage_length - 7 + regions = [[], [], [], [], [], [], [], [], [], [], []] for file in files: graph_bar_width = int(file['covered_percent'] / 100.0 * graph_width) filename = file['filename'].ljust(max_filename_length) - graph = ''.ljust(graph_bar_width, '█') + ''.ljust(graph_width - graph_bar_width) + graph = ''.ljust(graph_bar_width) coverage = '{:>5.1f}%'.format(file['covered_percent']) - output += '{} {} {}\n'.format(filename, graph, coverage) - - return output + graph_region_start = len(output) + max_filename_length + len(coverage) + 2 + output += '{} {} {}\n'.format(filename, coverage, graph) + + decile = int(file['covered_percent'] / 10) + regions[decile].append(sublime.Region(graph_region_start, graph_region_start + graph_bar_width)) + + return output, regions + + def apply_regions(self, regions): + view = self.panel + + for decile, decile_regions in list(enumerate(regions)): + decile_percent = decile * 10 + view.add_regions('coverage-graph-{}'.format(decile_percent), + decile_regions, + 'coverage.graph.{}'.format(decile_percent)) + + def augment_color_scheme(self): + """ + Generate a new color scheme from the original with additional coverage- + related style rules added. Save this color scheme to disk and set it as + the target view's active color scheme. + + (Hat tip to GitSavvy for this technique!) + """ + view = self.panel + + settings = view.settings() + self.restore_color_scheme() + original_color_scheme = settings.get("color_scheme") + settings.set("ruby_coverage.original_color_scheme", original_color_scheme) + colors = sublime.load_settings("SublimeRubyCoverage.sublime-settings").get("colors") + themeGenerator = ThemeGenerator(original_color_scheme) + themeGenerator.add_scoped_style( + "Coverage bar graph 0-9%", + "coverage.graph.0", + foreground = colors["graph"]["0"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 10-19%", + "coverage.graph.10", + foreground = colors["graph"]["10"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 20-29%", + "coverage.graph.20", + foreground = colors["graph"]["20"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 30-39%", + "coverage.graph.30", + foreground = colors["graph"]["30"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 40-49%", + "coverage.graph.40", + foreground = colors["graph"]["40"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 50-59%", + "coverage.graph.50", + foreground = colors["graph"]["50"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 60-69%", + "coverage.graph.60", + foreground = colors["graph"]["60"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 70-79%", + "coverage.graph.70", + foreground = colors["graph"]["70"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 80-89%", + "coverage.graph.80", + foreground = colors["graph"]["80"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 90-99%", + "coverage.graph.90", + foreground = colors["graph"]["90"], + background = "#1B1E22" + ) + themeGenerator.add_scoped_style( + "Coverage bar graph 100%", + "coverage.graph.100", + foreground = colors["graph"]["100"], + background = "#1B1E22" + ) + themeGenerator.apply_new_theme("ruby-coverage-graph", view) + + def restore_color_scheme(self): + settings = self.panel.settings() + original_color_scheme = settings.get("ruby_coverage.original_color_scheme") + if original_color_scheme: + settings.set("color_scheme", original_color_scheme) + settings.erase("ruby_coverage.original_color_scheme") From 08c5eafe4359c59495e3c786a3ef6c3de57c2234 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 12:35:17 +1000 Subject: [PATCH 35/41] Improve graph color gradient. --- SublimeRubyCoverage.sublime-settings | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/SublimeRubyCoverage.sublime-settings b/SublimeRubyCoverage.sublime-settings index b9ff52a..7f63452 100644 --- a/SublimeRubyCoverage.sublime-settings +++ b/SublimeRubyCoverage.sublime-settings @@ -34,17 +34,17 @@ Colors used in coverage bar graph. */ "graph": { - "0": "#FF0000", - "10": "#FC1807", - "20": "#FC2F07", - "30": "#FC4E07", - "40": "#FEC309", - "50": "#FFFF0A", - "60": "#C3FF09", - "70": "#8CFF08", - "80": "#5BFF07", - "90": "#35FF06", - "100": "#22FF06" + "0": "#FB0109", + "10": "#FB130A", + "20": "#FB360A", + "30": "#FB5B0A", + "40": "#FB7A0A", + "50": "#FD920A", + "60": "#FDB70B", + "70": "#FEE00A", + "80": "#FEFE0B", + "90": "#90FE09", + "100": "#36FF07" } }, From 7be22138067d7f6246af46a68c52f7e454e4667e Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 12:42:43 +1000 Subject: [PATCH 36/41] Display correct coverage graph dimensions on initial panel creation. --- show_project_ruby_coverage.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index a14655b..43b7470 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -25,20 +25,20 @@ def create_output_panel(self): self.panel = self.view.window().create_output_panel(PANEL_NAME) def display_project_coverage(self, edit): + panel = self.panel + panel.show(0) + self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) + output, regions = self.format_project_coverage() - panel = self.panel panel.set_read_only(False) panel.erase(edit, sublime.Region(0, panel.size())) panel.insert(edit, 0, output) panel.set_read_only(True) - panel.show(0) self.augment_color_scheme() self.apply_regions(regions) - self.view.window().run_command("show_panel", {"panel": "output.{}".format(PANEL_NAME)}) - def format_project_coverage(self): panel = self.panel files = self.coverage['files'] From bd3869cab74230e399848fbf14e91723d79f70ae Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 13:20:16 +1000 Subject: [PATCH 37/41] Add compact project coverage graph layout when viewport is narrow. --- show_project_ruby_coverage.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index 43b7470..00d5f66 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -45,26 +45,34 @@ def format_project_coverage(self): output = '' - viewport_width = int(panel.viewport_extent()[0] / panel.em_width()) + compact_layout = False + viewport_width = int(panel.viewport_extent()[0] / panel.em_width()) - 3 max_filename_length = len(max(files, key=lambda file: len(file['filename']))['filename']) - coverage_length = len('100%') - graph_width = viewport_width - max_filename_length - coverage_length - 7 + coverage_length = len(' 99.9%') + graph_width = viewport_width - max_filename_length - coverage_length - 1 - regions = [[], [], [], [], [], [], [], [], [], [], []] + if graph_width < 11: + # compact layout + compact_layout = True + max_filename_length = max(max_filename_length, viewport_width - coverage_length) + graph_width = max_filename_length + + graph_regions = [[], [], [], [], [], [], [], [], [], [], []] for file in files: graph_bar_width = int(file['covered_percent'] / 100.0 * graph_width) filename = file['filename'].ljust(max_filename_length) - graph = ''.ljust(graph_bar_width) - coverage = '{:>5.1f}%'.format(file['covered_percent']) + decimal_places = 1 if file['covered_percent'] < 100 else 0 + coverage = ('{:>' + str(coverage_length - 1) + '.' + str(decimal_places) + 'f}%').format(file['covered_percent']) - graph_region_start = len(output) + max_filename_length + len(coverage) + 2 - output += '{} {} {}\n'.format(filename, coverage, graph) + graph_region_start = len(output) + graph_region_start += max_filename_length + len(coverage) + 1 if not compact_layout else 0 + output += '{}{}'.format(filename, coverage).ljust(viewport_width) + '\n' decile = int(file['covered_percent'] / 10) - regions[decile].append(sublime.Region(graph_region_start, graph_region_start + graph_bar_width)) + graph_regions[decile].append(sublime.Region(graph_region_start, graph_region_start + graph_bar_width)) - return output, regions + return output, graph_regions def apply_regions(self, regions): view = self.panel From ae20c60286b0fdf846a06e86df62c55ae7d102f5 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 14:31:26 +1000 Subject: [PATCH 38/41] Split project coverage graph layouts into separate methods. --- show_project_ruby_coverage.py | 42 ++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index 00d5f66..c01459a 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -43,20 +43,21 @@ def format_project_coverage(self): panel = self.panel files = self.coverage['files'] - output = '' - - compact_layout = False viewport_width = int(panel.viewport_extent()[0] / panel.em_width()) - 3 max_filename_length = len(max(files, key=lambda file: len(file['filename']))['filename']) coverage_length = len(' 99.9%') - graph_width = viewport_width - max_filename_length - coverage_length - 1 + graph_width = viewport_width - max_filename_length - coverage_length - 2 + + if graph_width > 10: + return self.format_project_coverage_full(files, viewport_width, max_filename_length, coverage_length) + else: + return self.format_project_coverage_compact(files, viewport_width, max_filename_length, coverage_length) - if graph_width < 11: - # compact layout - compact_layout = True - max_filename_length = max(max_filename_length, viewport_width - coverage_length) - graph_width = max_filename_length + def format_project_coverage_compact(self, files, viewport_width, max_filename_length, coverage_length): + max_filename_length = max(max_filename_length, viewport_width - coverage_length) + graph_width = max_filename_length + output = '' graph_regions = [[], [], [], [], [], [], [], [], [], [], []] for file in files: graph_bar_width = int(file['covered_percent'] / 100.0 * graph_width) @@ -66,14 +67,33 @@ def format_project_coverage(self): coverage = ('{:>' + str(coverage_length - 1) + '.' + str(decimal_places) + 'f}%').format(file['covered_percent']) graph_region_start = len(output) - graph_region_start += max_filename_length + len(coverage) + 1 if not compact_layout else 0 - output += '{}{}'.format(filename, coverage).ljust(viewport_width) + '\n' + output += '{}{}\n'.format(filename, coverage).ljust(viewport_width-1) decile = int(file['covered_percent'] / 10) graph_regions[decile].append(sublime.Region(graph_region_start, graph_region_start + graph_bar_width)) return output, graph_regions + def format_project_coverage_full(self, files, viewport_width, max_filename_length, coverage_length): + graph_width = viewport_width - max_filename_length - coverage_length - 2 + + output = '' + graph_regions = [[], [], [], [], [], [], [], [], [], [], []] + for file in files: + graph_bar_width = int(file['covered_percent'] / 100.0 * graph_width) + + filename = file['filename'].ljust(max_filename_length) + decimal_places = 1 if file['covered_percent'] < 100 else 0 + coverage = ('{:>' + str(coverage_length - 1) + '.' + str(decimal_places) + 'f}%').format(file['covered_percent']) + + graph_region_start = len(output) + max_filename_length + len(coverage) + 1 + output += '{}{} ┃'.format(filename, coverage).ljust(viewport_width - 1) + '┃\n' + + decile = int(file['covered_percent'] / 10) + graph_regions[decile].append(sublime.Region(graph_region_start + 1, graph_region_start + graph_bar_width)) + + return output, graph_regions + def apply_regions(self, regions): view = self.panel From 375dfd61b912d12dd397b2b884173d52a62c889f Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 15:27:48 +1000 Subject: [PATCH 39/41] Add licence. Update README for new features. --- LICENSE | 21 +++++++++ README.md | 64 ++++++++++++---------------- SublimeRubyCoverage.sublime-commands | 4 +- 3 files changed, 50 insertions(+), 39 deletions(-) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a154859 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Kevin Yank + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index d729cd4..9bd9a61 100644 --- a/README.md +++ b/README.md @@ -1,53 +1,43 @@ -SublimeRubyCoverage -==================== +Sublime Ruby Coverage +===================== -A plugin for Sublime Text 2/3 that can highlight lines of Ruby lacking test coverage. +A plugin for Sublime Text 3 for visualising SimpleCov code coverage data in your editor. + +Features +-------- + +* Toggle highlighting of covered (green) and uncovered (red) lines of code. + * Shade of green indicates coverage level (with configurable thresholds). + * Highlight colors configurable. +* View whole file and current line coverage statistics in the status bar. + * Can be disabled in user settings. +* View list of all covered files in project, from least to most coverage. + * Includes color-coded coverage graph (colors configurable). + * Supports wide and compact layouts depending on window width. Installation ------------ -You will need to set up the [simplecov-json](https://github.com/vicentllongo/simplecov-json) Simplecov formatter in your project. +First, you must have [SimpleCov](https://github.com/colszowka/simplecov) installed and configured for your project. -Set up [Sublime Package Control](http://wbond.net/sublime_packages/package_control) if you don't have it yet. +Next, install and set up the [simplecov-json](https://github.com/vicentllongo/simplecov-json) formatter. If you’re using SimpleCov 0.9 or later, you have the option of using multiple formatters, so you can continue to generate the default HTML report along with the JSON report required by this package. -Go to Tools > Command Palette. -Type `Package Control: Install Package` and hit enter. -Type `Ruby Coverage` and hit enter. +Finally, install Sublime Ruby Coverage using [Package Control](https://packagecontrol.io): + +1. With Package Control installed, go to Tools > Command Palette. +2. Select the **Package Control: Install Package** command and hit Enter. +3. Type **Ruby Coverage** and hit Enter. Usage ----- -To set color of the marks, add the following to your **color scheme** settings array: +Run your tests to generate a **coverage/coverage.json** file in your project. Then: - - name - coverage.uncovered - scope - coverage.uncovered - settings - - foreground - #ffff33 - - +* Move your cursor around in one of the project’s Ruby files to see file and line coverage info in the status bar. +* Open Command Palette and choose **Ruby Coverage: Toggle Coverage Highlight** to display file coverage as green and red colored highlights. By default, lines covered once are highlighted in dark green, lines covered twice are highlighted in brighter green, and lines covered 50 or more times are displayed in very bright green. Invoke the command again to turn highlights off. +* Open Command Palette and choose **Ruby Coverage: Show Project Coverage** to open a panel containing a list of covered Ruby files in your project, from least to most coverage, with a color-coded bar graph indicating the coverage for each file. Ignoring Files -------------- -Add a .covignore file to your project root in order to add custom ignores. - -Highlighting lines missing coverage ------------------------------------ - -When you open a .rb file, -SublimeRubyCoverage tries to find coverage information -and highlight all uncovered lines with an outline. - -It does this by looking in all parent directories -until it finds a `coverage/sublime-ruby-coverage` directory as produced by [simplecov-sublime-ruby-coverage](http://github.com/integrum/simplecov-sublime-ruby-coverage). -The coverage file is expected to have as many lines as the source file, with each line containing a 1 if the line is covered or a 0 if it is not. - -You can force a reload of the coverage information -and redraw of the outlines -by running the `show_ruby_coverage` command, -bound to super+shift+c by default. +Common “non-code” Ruby files, such as spec files, are ignored automatically. Add a .covignore file to your project root in order to add additional, custom ignores. diff --git a/SublimeRubyCoverage.sublime-commands b/SublimeRubyCoverage.sublime-commands index 2c3d02a..b3daf93 100644 --- a/SublimeRubyCoverage.sublime-commands +++ b/SublimeRubyCoverage.sublime-commands @@ -1,4 +1,4 @@ [ - { "caption": "RubyCoverage: Toggle Coverage Highlight", "command": "toggle_ruby_coverage"}, - { "caption": "RubyCoverage: Show Project Coverage", "command": "show_project_ruby_coverage"} + { "caption": "Ruby Coverage: Toggle Coverage Highlight", "command": "toggle_ruby_coverage"}, + { "caption": "Ruby Coverage: Show Project Coverage", "command": "show_project_ruby_coverage"} ] From a3d1c65b1a618b8157a7ee06270c7038bd4338cb Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 15:35:14 +1000 Subject: [PATCH 40/41] Get coverage for first open folder when no open file is available. --- show_project_ruby_coverage.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/show_project_ruby_coverage.py b/show_project_ruby_coverage.py index c01459a..e5f316a 100644 --- a/show_project_ruby_coverage.py +++ b/show_project_ruby_coverage.py @@ -18,6 +18,12 @@ def run(self, edit): def get_project_coverage(self): filename = self.view.file_name() + if filename is None: + window_folders = sublime.active_window().folders() + if not window_folders or not os.path.isdir(window_folders[0]): + return None + filename = window_folders[0] + r = JsonCoverageReader(filename) self.coverage = r.get_project_coverage() if r else None From 99e874ab4bf39a16abc74750694587963d7514a9 Mon Sep 17 00:00:00 2001 From: Kevin Yank Date: Mon, 15 Jun 2015 15:41:45 +1000 Subject: [PATCH 41/41] Only display Toggle Ruby Coverage command in Ruby files. --- toggle_ruby_coverage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/toggle_ruby_coverage.py b/toggle_ruby_coverage.py index 56c22bf..4def7fa 100644 --- a/toggle_ruby_coverage.py +++ b/toggle_ruby_coverage.py @@ -8,6 +8,9 @@ class ToggleRubyCoverageCommand(sublime_plugin.TextCommand): """Show/hide coverage of current file based on a previous coverage run.""" + def is_enabled(self): + return 'source.ruby' in self.view.scope_name(0) + def run(self, edit): if 'source.ruby' not in self.view.scope_name(0): return