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)