diff --git a/CHANGELOG.md b/CHANGELOG.md index 090a6d1..d043ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). Changelog muster from [olivierlacan/keep-a-changelog](https://github.com/olivierlacan/keep-a-changelog). +## [0.1.0] - 2016-12-26 +### Added +- handle multiple selections + ## [0.0.5] - 2016-12-26 ### Added - `sublime_command` spell diff --git a/context/context.py b/context/context.py new file mode 100644 index 0000000..11e6927 --- /dev/null +++ b/context/context.py @@ -0,0 +1,49 @@ +import re + + +def check_scope(current_scope, target_scopes): + valid = True + for target_scope in target_scopes: + if not re.search(target_scope, current_scope): + valid = False + break + return valid + + +def check_line_matches(line_text, target_parts): + valid = True + for target_part in target_parts: + if not re.search(target_part, line_text): + valid = False + break + return valid + + +def check(view, spell): + valid = True + + for sel in view.sel(): + line = view.line(sel.a) + line_text = view.substr(line) + view_scope = view.scope_name(sel.a) + + context = spell.get('context', {'scope': [], 'line_matches': []}) + spell_scope = context.get('scope', []) + line_matches = context.get('line_matches', []) + values = spell.get('args', {'values': None}).get('values', None) + + if values: + escaped_values = [re.escape(val) for val in values] + line_matches.append('(' + '|'.join(escaped_values) + ')') + + valid = check_scope(view_scope, spell_scope) + + if not valid: + break + + valid = check_line_matches(line_text, line_matches) + + if not valid: + break + + return valid diff --git a/context/context_checker.py b/context/context_checker.py deleted file mode 100644 index a7f2aaf..0000000 --- a/context/context_checker.py +++ /dev/null @@ -1,41 +0,0 @@ -import re - - -class ContextChecker(object): - - def __init__(self, view): - self.view = view - self.sel = self.view.sel()[0] - self.line = self.view.line(self.sel.a) - self.line_text = self.view.substr(self.line) - self.scope_name = self.view.scope_name(self.sel.a) - - def check(self, spell): - context = spell.get('context', {'scope': [], 'line_matches': []}) - scope = context.get('scope', []) - line_matches = context.get('line_matches', []) - values = spell.get('args', {'values': None}).get('values', None) - if values: - escaped_values = [re.escape(val) for val in values] - line_matches.append('(' + '|'.join(escaped_values) + ')') - valid = self.check_scope(scope) - if not valid: - return False - valid = self.check_line_matches(line_matches) - return valid - - def check_scope(self, target_scopes): - is_valid = True - for target_scope in target_scopes: - if not re.search(target_scope, self.scope_name): - is_valid = False - break - return is_valid - - def check_line_matches(self, target_parts): - is_valid = True - for target_part in target_parts: - if not re.search(target_part, self.line_text): - is_valid = False - break - return is_valid diff --git a/spells/perform_line_regex.py b/spells/perform_line_regex.py index ed07125..3a3c3bc 100644 --- a/spells/perform_line_regex.py +++ b/spells/perform_line_regex.py @@ -9,14 +9,15 @@ class PerformLineRegexSpell(MagicSpell): def cast(self): pattern = re.compile(self.spell.get('args').get('pattern')) - replacement = self.spell.get('args').get('replacement') - sel = self.view.sel()[0] - line = self.view.line(sel.a) + replacement = self.spell.get('args').get('replacement') if replacement == '$clipboard': replacement = sublime.get_clipboard() - line_text = self.view.substr(line) - new_line_text = re.sub(pattern, "%s" % replacement, line_text) + for sel in self.view.sel(): + line = self.view.line(sel.a) + + line_text = self.view.substr(line) + new_line_text = re.sub(pattern, "%s" % replacement, line_text) - return self.view.replace(self.edit, line, new_line_text) + self.view.replace(self.edit, line, new_line_text) diff --git a/spells/replace_text.py b/spells/replace_text.py index baa90b5..fa3ab55 100644 --- a/spells/replace_text.py +++ b/spells/replace_text.py @@ -2,6 +2,7 @@ import sublime from .magic_spell import MagicSpell +from ..utils import utils class ReplaceTextSpell(MagicSpell): @@ -9,61 +10,39 @@ class ReplaceTextSpell(MagicSpell): def cast(self): where = self.spell.get('args').get('where') - self.delimiter_length = len(self.spell.get('args').get('delimiter')) - self.delimiter = re.compile(self.spell.get('args').get('delimiter')) - self.replacement = self.spell.get('args').get('replacement') - self.sel = self.view.sel()[0] - self.line = self.view.line(self.sel.a) + delimiter = self.spell.get('args').get('delimiter') + delimiter_length = len(delimiter) + re_delimiter = re.compile(delimiter) + self.replacement = self.spell.get('args').get('replacement') if self.replacement == '$clipboard': self.replacement = sublime.get_clipboard() - if where == 'inside': - start = self.find_previous_delimiter(self.sel.a) - if start: - end = self.find_next_delimiter(self.sel.b) - if end: - return self.replace(start, end) - elif where == 'after': - start = self.find_first_delimiter_in_line() - if start: - start = start + self.delimiter_length - end = self.end_of_line() - if end: - return self.replace(start, end) - else: - raise AttributeError('Unknown value for "where": ' + where) - - def find_previous_delimiter(self, start): - found = None + for sel in self.view.sel(): + line = self.view.line(sel.a) - while start > self.line.a and start > 0: - region = sublime.Region(start - 1, start) - if self.delimiter.match(self.view.substr(region)): - found = start - break - start -= 1 + if where == 'inside': + start = utils.find_previous_delimiter( + self.view, line, re_delimiter, sel.a) - return found + if start: + end = utils.find_next_delimiter( + self.view, line, re_delimiter, delimiter_length, sel.b) + if end: + self.replace(start, end) - def find_next_delimiter(self, start): - found = None + elif where == 'after': + start = utils.find_next_delimiter( + self.view, line, re_delimiter, delimiter_length, line.a) + if start: + start = start + delimiter_length + end = line.b + if end: + return self.replace(start, end) - while start < self.line.b and start < 999999: - region = sublime.Region(start, start + self.delimiter_length) - if self.delimiter.match(self.view.substr(region)): - found = start - break - start += 1 - - return found - - def find_first_delimiter_in_line(self): - return self.find_next_delimiter(self.line.a) + else: + raise AttributeError('Unknown value for "where": ' + where) def replace(self, start, end): region = sublime.Region(start, end) self.view.replace(self.edit, region, self.replacement) - - def end_of_line(self): - return self.line.b diff --git a/spells/toggle_values.py b/spells/toggle_values.py index baa0167..0f21774 100644 --- a/spells/toggle_values.py +++ b/spells/toggle_values.py @@ -12,12 +12,11 @@ def cast(self): escaped_values = [re.escape(val) for val in self.values] pattern = '(' + '|'.join(escaped_values) + ')' - sel = self.view.sel()[0] - line = self.view.line(sel.a) - line_text = self.view.substr(line) - new_line_text = re.sub(pattern, self.replace, line_text) - - return self.view.replace(self.edit, line, new_line_text) + for sel in self.view.sel(): + line = self.view.line(sel.a) + line_text = self.view.substr(line) + new_line_text = re.sub(pattern, self.replace, line_text) + self.view.replace(self.edit, line, new_line_text) def replace(self, matchobj): index = self.values.index(matchobj.group(0)) + 1 diff --git a/sublime_magic.py b/sublime_magic.py index ec5d1c1..debfd86 100644 --- a/sublime_magic.py +++ b/sublime_magic.py @@ -2,17 +2,24 @@ import sublime import sublime_plugin from .messenger import messenger -from .context import context_checker from .spells import * +from .context import context class SublimeMagic(sublime_plugin.TextCommand): def run(self, edit): self.edit = edit - self.get_known_spells() + self.known_spells = [ + 'replace_text', + 'perform_line_regex', + 'toggle_values', + 'sublime_command' + ] + self.get_user_spells() - self.find_first_matching_spell() + self.find_first_matching_user_spell() + if self.spell is None: self.message('No spell found.') else: @@ -21,24 +28,15 @@ def run(self, edit): except NotImplementedError as e: self.message(str(e)) - def get_known_spells(self): - # TODO: can we do this better? - self.known_spells = [] - for name, val in globals().items(): - if hasattr(val, '__name__'): - if 'SublimeMagic.spells' in val.__name__: - self.known_spells.append(name) - def get_user_spells(self): self.magic_settings = sublime.load_settings( 'SublimeMagic.sublime-settings') self.user_spells = self.magic_settings.get('spells') - def find_first_matching_spell(self): + def find_first_matching_user_spell(self): self.spell = None - checker = context_checker.ContextChecker(self.view) for spell in self.user_spells: - if checker.check(spell): + if context.check(self.view, spell): self.spell = spell break @@ -55,13 +53,13 @@ def cast_spell(self): if not spell_name in self.known_spells: raise NotImplementedError('Unknown spell: ' + spell_name) try: - spell_fn = eval( + spell_class = eval( spell_name + '.' + self.spell_to_class(spell_name) + 'Spell') - spell_fn = spell_fn(self.edit, self.view, self.spell) - spell_fn.cast() + spell_class = spell_class(self.edit, self.view, self.spell) + spell_class.cast() name = self.spell.get('name', None) if not name: name = spell_name diff --git a/tests/test_context_checker.py b/tests/test_context_checker.py index 21491c8..c528b8d 100644 --- a/tests/test_context_checker.py +++ b/tests/test_context_checker.py @@ -2,36 +2,29 @@ import sys from base import SublimeMagicTestCase +from SublimeMagic.context import context -context_checker_module = sys.modules['SublimeMagic.context.context_checker'] +def c(a): + return { + 'context': a + } -class TestContextChecker(SublimeMagicTestCase): - - def test_scope_name(self): - checker = context_checker_module.ContextChecker(self.view) - self.assertEqual(checker.scope_name, 'text.plain ') - def test_line_text(self): - self.setText('foo bar') - checker = context_checker_module.ContextChecker(self.view) - self.assertEqual(checker.line_text, 'foo bar') +class TestContextChecker(SublimeMagicTestCase): def test_check_scope(self): - checker = context_checker_module.ContextChecker(self.view) - - target_scopes = ['plain'] - self.assertTrue(checker.check_scope(target_scopes)) + test_context = c({'scope': ['plain']}) + self.assertTrue(context.check(self.view, test_context)) - target_scopes = ['foobar'] - self.assertFalse(checker.check_scope(target_scopes)) + test_context = c({'scope': ['foobar']}) + self.assertFalse(context.check(self.view, test_context)) def test_check_line_matches(self): self.setText('foo bar') - checker = context_checker_module.ContextChecker(self.view) - target_parts = ['foo', 'bar', 'foo bar'] - self.assertTrue(checker.check_line_matches(target_parts)) + test_context = c({'line_matches': ['foo', 'bar', 'foo bar']}) + self.assertTrue(context.check(self.view, test_context)) - target_parts = ['foobar'] - self.assertFalse(checker.check_line_matches(target_parts)) + test_context = c({'line_matches': ['foobar']}) + self.assertFalse(context.check(self.view, test_context)) diff --git a/utils/utils.py b/utils/utils.py new file mode 100644 index 0000000..329c872 --- /dev/null +++ b/utils/utils.py @@ -0,0 +1,27 @@ +import sublime + + +def find_previous_delimiter(view, line, re_delimiter, start): + found = None + + while start > line.a and start > 0: + region = sublime.Region(start - 1, start) + if re_delimiter.match(view.substr(region)): + found = start + break + start -= 1 + + return found + + +def find_next_delimiter(view, line, re_delimiter, delimiter_length, start): + found = None + + while start < line.b and start < 999999: + region = sublime.Region(start, start + delimiter_length) + if re_delimiter.match(view.substr(region)): + found = start + break + start += 1 + + return found