diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2500b0f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +*.cache \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e09e61d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +### 0.0.1 + +* Package released diff --git a/ESpec.tmLanguage b/ESpec.tmLanguage new file mode 100644 index 0000000..7946ae3 --- /dev/null +++ b/ESpec.tmLanguage @@ -0,0 +1,194 @@ + + + + + fileTypes + + exs + + foldingStartMarker + (?x)^ + (\s*+ + (module|class|def + |background|feature|subscribe + |before|describe|it|scenario + |unless|if + |case + |begin + |for|while|until + |^=begin + |( "(\\.|[^"])*+" # eat a double quoted string + | '(\\.|[^'])*+' # eat a single quoted string + | [^#"'] # eat all but comments and strings + )* + ( \s (do|begin|case) + | (?<!\$)[-+=&|*/~%^<>~] \s*+ (if|unless) + ) + )\b + (?! [^;]*+ ; .*? \bend\b ) + |( "(\\.|[^"])*+" # eat a double quoted string + | '(\\.|[^'])*+' # eat a single quoted string + | [^#"'] # eat all but comments and strings + )* + ( \{ (?! [^}]*+ \} ) + | \[ (?! [^\]]*+ \] ) + ) + ).*$ + | [#] .*? \(fold\) \s*+ $ # Sune’s special marker + + foldingStopMarker + (?x) + ( (^|;) \s*+ end \s*+ ([#].*)? $ + | (^|;) \s*+ end \. .* $ + | ^ \s*+ [}\]] \s*+ ([#].*)? $ + | [#] .*? \(end\) \s*+ $ # Sune’s special marker + | ^=end + ) + keyEquivalent + ^~R + name + ESpec + patterns + + + match + (?<!\.)\b(before\b|after\b|subject\b!?|let\b!?) + name + keyword.other.espec + + + include + #behaviour + + + include + #single-line-example + + + include + #pending + + + include + #example + + + include + source.elixir + + + repository + + behaviour + + begin + ^\s*(?:(ESpec)\.)?(describe|context|feature)\b + beginCaptures + + 1 + + name + support.class.elixir + + 2 + + name + keyword.other.espec.behaviour + + + end + \b(do(?=\s*$))|{ + endCaptures + + 1 + + name + keyword.control.elixir.start-block + + + name + meta.espec.behaviour + patterns + + + include + source.elixir + + + + example + + begin + ^\s*(it|specify|scenario)\b + beginCaptures + + 1 + + name + keyword.other.espec.example + + + end + \b(do(?=\s*$))|{ + endCaptures + + 1 + + name + keyword.control.elixir.start-block + + + name + meta.espec.example + patterns + + + include + source.elixir + + + + pending + + begin + ^\s*(it|specify|scenario)\b(?=((?!do|{).)*$) + end + $ + beginCaptures + + 1 + + name + keyword.other.espec.pending + + + patterns + + + include + source.elixir + + + name + meta.espec.pending + + single-line-example + + captures + + 1 + + name + keyword.other.espec.example + + + match + ^\s*(it|specify|scenario)\s*{ + + + scopeName + source.elixir.espec + uuid + 923F0A10-96B9-4792-99A4-94FEF66E0B8C + + diff --git a/ESpecCreateModule.py b/ESpecCreateModule.py new file mode 100644 index 0000000..4a7d58b --- /dev/null +++ b/ESpecCreateModule.py @@ -0,0 +1,90 @@ +import sublime, sublime_plugin, time +import re + +from textwrap import dedent +from ESpec.shared import other_group_in_pair + + +def snake_case(name): + s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() + + +class GotoLineCommand(sublime_plugin.TextCommand): + + def run(self, edit, line, column=0): + pt = self.view.text_point(line - 1, column) + + self.view.sel().clear() + self.view.sel().add(sublime.Region(pt)) + + self.view.show(pt) + + +class EspecNewModuleCommand(sublime_plugin.TextCommand): + + def run(self, edit, name, namespace): + class_template = dedent(''' + class {name} + + end + '''.lstrip('\n').rstrip(' \n').format(name=name)) + + module_template = dedent(''' + module {module} + {definition} + end + '''.lstrip('\n').rstrip(' \n')) + + template, level = class_template, len(namespace) + + while namespace: + module = namespace.pop() + template = module_template.format(module=module, definition=self.indent(template)) + + self.view.insert(edit, 0, template) + self.view.run_command('goto_line', { 'line': 2 + level, 'column': level * 2 }) + + def indent(self, text, space=2): + return '\n'.join(' ' * space + line for line in text.split('\n')) + + +class EspecNewSpecCommand(sublime_plugin.TextCommand): + + def run(self, edit, name): + template = dedent(''' + require 'spec_helper' + + describe {name} do + + end + '''.strip('\n').format(name=name)) + + self.view.insert(edit, 0, template) + self.view.run_command('goto_line', { 'line': 4 }) + + +class EspecCreateModuleCommand(sublime_plugin.WindowCommand): + + def run(self): + self.window.show_input_panel("Enter module name:", "", self.on_done, None, None) + + def on_done(self, text): + if not text: return + + *namespace, name = re.split(r'/|::', text.strip(' _/')) + + # create the module + module = self.window.new_file() + module.set_syntax_file('Packages/Ruby/Ruby.tmLanguage') + module.set_name(snake_case(name) + '.rb') + + module.run_command('rspec_new_module', { 'name': name, 'namespace': namespace }) + + # create the spec + spec = self.window.new_file() + self.window.run_command('move_to_group', { 'group': other_group_in_pair(self.window) }) + spec.set_syntax_file('Packages/Ruby/Ruby.tmLanguage') + spec.set_name(snake_case(name) + '_spec.rb') + + spec.run_command('rspec_new_spec', { 'name': '::'.join(namespace + [name]) }) diff --git a/ESpecDetectFileType.py b/ESpecDetectFileType.py new file mode 100644 index 0000000..ea6a179 --- /dev/null +++ b/ESpecDetectFileType.py @@ -0,0 +1,26 @@ +import sublime, sublime_plugin +import os + +class ESpecDetectFileTypeCommand(sublime_plugin.EventListener): + ''' + Detects current file type if the file's extension + ''' + + def on_load(self, view): + filename = view.file_name() + + if not filename: return # not saved + + name = os.path.basename(filename.lower()) + if name.endswith("_spec.exs"): + set_syntax(view, "ESpec") + elif name == "factories.exs": + set_syntax(view, "ESpec") + + +def set_syntax(view, syntax, path=None): + if path is None: + path = syntax + + view.settings().set('syntax', 'Packages/'+ path + '/' + syntax + '.tmLanguage') + print("Switched syntax to: " + syntax) diff --git a/OpenESpecFile.py b/OpenESpecFile.py new file mode 100644 index 0000000..6b0563a --- /dev/null +++ b/OpenESpecFile.py @@ -0,0 +1,85 @@ +import sublime +import sublime_plugin +import re, inspect, os +from ESpec import shared + +class OpenEspecFileCommand(sublime_plugin.WindowCommand): + + def run(self): + if not self.window.active_view(): + return + + current_file_path = self.window.active_view().file_name() + if current_file_path is None: return + + print("Current file: " + current_file_path) + + if current_file_path.endswith(".exs"): + if self.quick_find(current_file_path): + return + + current_file_name = re.search(r"[/\\]([\w.]+)$", current_file_path).group(1) + base_name = re.search(r"(\w+)\.exs$", current_file_name).group(1) + base_name = re.sub(r"_spec$", "", base_name) + + if current_file_name.endswith("_spec.exs"): + source_matcher = re.compile(r"[/\\]" + base_name + "\.exs$") + self.open_project_file(source_matcher, current_file_path) + else: + test_matcher = re.compile(r"[/\\]" + base_name + "_spec\.exs$") + self.open_project_file(test_matcher, current_file_path) + else: + print("Error: current file is not a elixir file") + + def open_project_file(self, file_matcher, file_path): + for path, dirs, filenames in self.walk_project_folder(file_path): + for filename in filter(lambda f: f.endswith(".exs"), filenames): + current_file = os.path.join(path, filename) + if file_matcher.search(current_file): + return self.switch_to(os.path.join(path, filename)) + print("ESpec: No matching files found") + + def spec_paths(self, file_path): + return [ + self.batch_replace(file_path, + (r"\b(?:app|lib)\b", "spec"), (r"\b(\w+)\.exs", r"\1_spec.exs")), + self.batch_replace(file_path, + (r"\blib\b", os.path.join("spec", "lib")), (r"\b(\w+)\.exs", r"\1_spec.exs")) + ] + + def code_paths(self, file_path): + file_path = re.sub(r"\b(\w+)_spec\.exs$", r"\1.exs", file_path) + return [ + re.sub(r"\bspec\b", "app", file_path), + re.sub(r"\bspec\b", "lib", file_path), + re.sub(r"\b{}\b".format(os.path.join("spec", "lib")), "lib", file_path) + ] + + def quick_find(self, file_path): + if re.search(r"\bspec\b|_spec\.exs$", file_path): + for path in self.code_paths(file_path): + if os.path.exists(path): + return self.switch_to(path) + elif re.search(r"\b(?:app|lib)\b", file_path): + for path in self.spec_paths(file_path): + if os.path.exists(path): + return self.switch_to(path) + print("ESpec: quick find failed, doing regular find") + + def batch_replace(self, string, *pairs): + for target, replacement in pairs: + string = re.sub(target, replacement, string) + return string + + def switch_to(self, file_path): + group = shared.other_group_in_pair(self.window) + file_view = self.window.open_file(file_path) + self.window.run_command("move_to_group", { "group": group }) + print("Opened: " + file_path) + return True + + def walk_project_folder(self, file_path): + for folder in self.window.folders(): + if not file_path.startswith(folder): + continue + yield from os.walk(folder) diff --git a/Preferences/SymbolList-Behaviour.tmPreferences b/Preferences/SymbolList-Behaviour.tmPreferences new file mode 100644 index 0000000..9c0fe74 --- /dev/null +++ b/Preferences/SymbolList-Behaviour.tmPreferences @@ -0,0 +1,19 @@ + + + + + name + Symbol List: Behaviour + scope + meta.espec.behaviour + settings + + showInSymbolList + 1 + symbolTransformation + s/^\s*(describe)\s+(.+)\s+do\s*$/$2/ + + uuid + 28F89786-04F4-43D7-82A6-34B046C2BC6B + + diff --git a/Preferences/SymbolList-Example.tmPreferences b/Preferences/SymbolList-Example.tmPreferences new file mode 100644 index 0000000..d7f637d --- /dev/null +++ b/Preferences/SymbolList-Example.tmPreferences @@ -0,0 +1,19 @@ + + + + + name + Symbol List: Example + scope + meta.espec.example + settings + + showInSymbolList + 1 + symbolTransformation + s/^\s*(it)\s+(.+)\s+do\s*$/ $2/ + + uuid + 57EF6130-05A6-4117-94CB-C0BD63328334 + + diff --git a/Preferences/SymbolList-Pending.tmPreferences b/Preferences/SymbolList-Pending.tmPreferences new file mode 100644 index 0000000..837bf9a --- /dev/null +++ b/Preferences/SymbolList-Pending.tmPreferences @@ -0,0 +1,19 @@ + + + + + name + Symbol List: Pending + scope + meta.espec.pending + settings + + showInSymbolList + 1 + symbolTransformation + s/^\s*(it)\s+(.+)\s*$/ $2 (Pending)/ + + uuid + 377BD4F9-4321-4D14-9A34-6A93033F1906 + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcd9221 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# ESpec Package for Sublime Text 2/3 + +Based on the awesome [RSpec Package for Sublime Text 2/3][https://github.com/SublimeText/RSpec] + +## Description + +[ESpec][espec] is a Behavior-Driven Development testing framework for Elixir. +ESpec is inspired by RSpec and the main idea is to be close to its perfect DSL. +This package adds support to Sublime Text 2 and 3 for specifying and testing Elixir applications with ESpec. +It contains extra syntax highlighting and many snippets. + +[rspec]: https://github.com/antonmi/espec + +## Installation + + + +## Features + +* RSpec.tmLanguage: plugin automatically uses *ESpec language syntax* when you are in a ESpec file +* Large amount of *ESpec* snippets + +##Contributing + +* Pull requests are welcome! diff --git a/messages.json b/messages.json new file mode 100644 index 0000000..fedadea --- /dev/null +++ b/messages.json @@ -0,0 +1,3 @@ +{ + "0.0.1": "messages/0.0.1.txt", +} diff --git a/messages/0.0.1.txt b/messages/0.0.1.txt new file mode 100644 index 0000000..717a081 --- /dev/null +++ b/messages/0.0.1.txt @@ -0,0 +1,11 @@ +============ 2014-11-06 ============= + +ESpec package initial release + +Recent changes: + +* package created + +Please file bug reports at https://github.com/SublimeText/RSpec/issues + +This release was done by Po Chen (@princemaple), feel free to @ when reporting bugs and sending PRs. diff --git a/shared.py b/shared.py new file mode 100644 index 0000000..3514201 --- /dev/null +++ b/shared.py @@ -0,0 +1,7 @@ +"""Returns the neighbour focus group for the current window.""" +def other_group_in_pair(window): + if window.active_group() % 2 == 0: + target_group = window.active_group() + 1 + else: + target_group = window.active_group() - 1 + return min(target_group, window.num_groups() - 1)