diff --git a/Arigato.pyproj b/Arigato.pyproj
new file mode 100644
index 0000000..6bbe3a1
--- /dev/null
+++ b/Arigato.pyproj
@@ -0,0 +1,259 @@
+
+
+
+ Debug
+ 2.0
+ {64fd19f3-e336-46d9-a759-e17db2b6443c}
+
+
+
+ .
+ .
+ 10.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Arigato.sln b/Arigato.sln
new file mode 100644
index 0000000..7b1cda9
--- /dev/null
+++ b/Arigato.sln
@@ -0,0 +1,29 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 2012
+Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "Arigato", "Arigato.pyproj", "{64FD19F3-E336-46D9-A759-E17DB2B6443C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Definition Files", "Definition Files", "{E4580E16-7E49-4ECC-9AC2-E63B1E6DE092}"
+ ProjectSection(SolutionItems) = preProject
+ Context.sublime-menu = Context.sublime-menu
+ Default (Windows).sublime-keymap = Default (Windows).sublime-keymap
+ Default (Windows).sublime-mousemap = Default (Windows).sublime-mousemap
+ Main.sublime-menu = Main.sublime-menu
+ package-metadata.json = package-metadata.json
+ robot-output.tmLanguage = robot-output.tmLanguage
+ robot.tmLanguage = robot.tmLanguage
+ EndProjectSection
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {64FD19F3-E336-46D9-A759-E17DB2B6443C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {64FD19F3-E336-46D9-A759-E17DB2B6443C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+EndGlobal
diff --git a/Context.sublime-menu b/Context.sublime-menu
new file mode 100644
index 0000000..5f01cf6
--- /dev/null
+++ b/Context.sublime-menu
@@ -0,0 +1,116 @@
+[
+ {
+ "caption": "Go to definition",
+ "command": "robot_go_to_keyword"
+ }
+ , {
+ "caption": "Find references",
+ "command": "robot_find_references"
+ }
+ , {
+ "caption": "Replace references",
+ "command": "prompt_robot_replace_references"
+ }
+ , {
+ "caption": "Run test",
+ "command": "robot_run_test"
+ }
+ , {
+ "caption": "Run test suite",
+ "command": "robot_run_test_suite"
+ }
+ , {
+ "caption": "Run...",
+ "command": "robot_run_panel"
+ }
+ , {
+ "caption": "Code Snippets",
+ "children":
+ [
+ {
+ "caption": "*** Settings ***",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/heading-settings.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "*** Variables ***",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/heading-variables.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "*** Test Cases ***",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/heading-testcases.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "*** Keywords ***",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/heading-keywords.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "[DOCUMENTATION]",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/documentation.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "[Arguments]",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/arguments.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": ":FOR Loop",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/forloop.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": ":FOR Loop in Range",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/forloopinrange.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "Import OperatingSystem",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/library-operatingsystem.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "Import Selenium2Library",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/library-selenium2library.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "Import JSON Library",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/library-json.sublime-snippet"
+ }
+ }
+ ,{
+ "caption": "Import XML Library",
+ "command": "insert_snippet",
+ "args": {
+ "name": "Packages/Robot Framework/Snippets/library-xml.sublime-snippet"
+ }
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/Default (Windows).sublime-keymap b/Default (Windows).sublime-keymap
index ba9ac87..d9cb6e0 100644
--- a/Default (Windows).sublime-keymap
+++ b/Default (Windows).sublime-keymap
@@ -1,3 +1,5 @@
[
- { "keys": ["alt+enter"], "command": "robot_go_to_keyword" }
+ { "keys": ["alt+enter"], "command": "robot_go_to_keyword" },
+ { "keys": ["$","{","{"], "command": "robot_complete_variable" },
+ { "keys": ["@","{","{"], "command": "robot_complete_list" }
]
diff --git a/Main.sublime-menu b/Main.sublime-menu
index caa4df6..047c6d4 100644
--- a/Main.sublime-menu
+++ b/Main.sublime-menu
@@ -113,6 +113,10 @@
"caption": "Mouse Bindings - User"
},
{ "caption": "-" }
+ , {
+ "caption": "Run options",
+ "command": "robot_run_options"
+ }
]
}
]
diff --git a/Snippets/arguments.sublime-snippet b/Snippets/arguments.sublime-snippet
new file mode 100644
index 0000000..0dbd080
--- /dev/null
+++ b/Snippets/arguments.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ :
+
+ text.robot
+ Robot Framework [Arguments]
+
+
diff --git a/Snippets/documentation.sublime-snippet b/Snippets/documentation.sublime-snippet
new file mode 100644
index 0000000..f3da245
--- /dev/null
+++ b/Snippets/documentation.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ :
+
+ text.robot
+ Robot Framework [Documentation]
+
+
diff --git a/Snippets/forloop.sublime-snippet b/Snippets/forloop.sublime-snippet
new file mode 100644
index 0000000..395d371
--- /dev/null
+++ b/Snippets/forloop.sublime-snippet
@@ -0,0 +1,12 @@
+
+
+
+ :f
+
+ text.robot
+ Robot Framework :FOR loop
+
+
diff --git a/Snippets/forloopinrange.sublime-snippet b/Snippets/forloopinrange.sublime-snippet
new file mode 100644
index 0000000..3330003
--- /dev/null
+++ b/Snippets/forloopinrange.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ :f
+
+ text.robot
+ Robot Framework :FOR loop in range
+
\ No newline at end of file
diff --git a/Snippets/heading-keywords.sublime-snippet b/Snippets/heading-keywords.sublime-snippet
new file mode 100644
index 0000000..f5c9c91
--- /dev/null
+++ b/Snippets/heading-keywords.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *k
+
+ text.robot
+ Robot Framework *** Keywords *** table
+
\ No newline at end of file
diff --git a/Snippets/heading-settings.sublime-snippet b/Snippets/heading-settings.sublime-snippet
new file mode 100644
index 0000000..be1cdfe
--- /dev/null
+++ b/Snippets/heading-settings.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *s
+
+ text.robot
+ Robot Framework *** Settings *** table
+
\ No newline at end of file
diff --git a/Snippets/heading-testcases.sublime-snippet b/Snippets/heading-testcases.sublime-snippet
new file mode 100644
index 0000000..0bab044
--- /dev/null
+++ b/Snippets/heading-testcases.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *t
+
+ text.robot
+ Robot Framework *** Test Cases *** table
+
\ No newline at end of file
diff --git a/Snippets/heading-variables.sublime-snippet b/Snippets/heading-variables.sublime-snippet
new file mode 100644
index 0000000..7879a38
--- /dev/null
+++ b/Snippets/heading-variables.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *v
+
+ text.robot
+ Robot Framework *** Variables *** table
+
\ No newline at end of file
diff --git a/Snippets/library-json.sublime-snippet b/Snippets/library-json.sublime-snippet
new file mode 100644
index 0000000..20329c7
--- /dev/null
+++ b/Snippets/library-json.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *js
+
+ text.robot
+ Robot Framework Library JSON(HttpLibrary.HTTP)
+
\ No newline at end of file
diff --git a/Snippets/library-operatingsystem.sublime-snippet b/Snippets/library-operatingsystem.sublime-snippet
new file mode 100644
index 0000000..e5ade18
--- /dev/null
+++ b/Snippets/library-operatingsystem.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *os
+
+ text.robot
+ Robot Framework Library OperatingSyetem
+
\ No newline at end of file
diff --git a/Snippets/library-selenium2library.sublime-snippet b/Snippets/library-selenium2library.sublime-snippet
new file mode 100644
index 0000000..20ed985
--- /dev/null
+++ b/Snippets/library-selenium2library.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *sl
+
+ text.robot
+ Robot Framework Library Selenium2Library
+
\ No newline at end of file
diff --git a/Snippets/library-xml.sublime-snippet b/Snippets/library-xml.sublime-snippet
new file mode 100644
index 0000000..ac21e84
--- /dev/null
+++ b/Snippets/library-xml.sublime-snippet
@@ -0,0 +1,11 @@
+
+
+
+ *x
+
+ text.robot
+ Robot Framework Library XML
+
\ No newline at end of file
diff --git a/lib/keyword_parse.py b/lib/keyword_parse.py
deleted file mode 100644
index 84e959d..0000000
--- a/lib/keyword_parse.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import unittest
-
-def get_keyword_at_pos(line, col):
- length = len(line)
-
- if length == 0:
- return None
-
- # between spaces
- if ((col >= length or line[col] == ' ' or line[col] == "\t")
- and (col == 0 or line[col-1] == ' ' or line[col-1] == "\t")):
- return None
-
- # first look back until we find 2 spaces in a row, or reach the beginning
- i = col - 1
- while i >= 0:
- if line[i] == "\t" or ((line[i - 1] == ' ' or line[i - 1] == '|') and line[i] == ' '):
- break
- i -= 1
- begin = i + 1
-
- # now look forward or until the end
- i = col # previous included line[col]
- while i < length:
- if line[i] == "\t" or (line[i] == " " and len(line) > i and (line[i + 1] == " " or line[i + 1] == '|')):
- break
- i += 1
- end = i
-
- keyword = line[begin:end]
-
- return line[begin:end]
-
-class TestGetKeywordAtPos(unittest.TestCase):
- def test_edges(self):
- self.assertEqual(get_keyword_at_pos('', 0), None)
- self.assertEqual(get_keyword_at_pos('A', 0), 'A')
- self.assertEqual(get_keyword_at_pos('A', 1), 'A')
- for i in range(0, 3):
- self.assertEqual(get_keyword_at_pos('AB', i), 'AB')
- for i in range(0, 4):
- self.assertEqual(get_keyword_at_pos('A B', i), 'A B')
- self.assertEqual(get_keyword_at_pos(' A', 4), 'A')
- self.assertEqual(get_keyword_at_pos('A ', 0), 'A')
-
- def test_splitting(self):
- self.assertEqual(get_keyword_at_pos('ABC DEF', 1), 'ABC')
- self.assertEqual(get_keyword_at_pos('ABC DEF', 5), 'DEF')
- self.assertEqual(get_keyword_at_pos('ABC DEF', 6), 'DEF')
- self.assertEqual(get_keyword_at_pos(' ABC DEF ', 3), 'ABC')
- self.assertEqual(get_keyword_at_pos(' ABC DEF ', 8), 'DEF')
-
- def test_inbetween_spaces(self):
- self.assertEqual(get_keyword_at_pos('ABC DEF', 4), None)
- self.assertEqual(get_keyword_at_pos(' ', 0), None)
- self.assertEqual(get_keyword_at_pos(' ', 1), None)
- self.assertEqual(get_keyword_at_pos(' ', 2), None)
- self.assertEqual(get_keyword_at_pos(' A', 0), None)
-
- def test_samples(self):
- self.assertEqual(get_keyword_at_pos('This Is A Keyword', 3), 'This Is A Keyword')
- self.assertEqual(get_keyword_at_pos('This Is A Keyword', 17), 'This Is A Keyword')
- self.assertEqual(get_keyword_at_pos('Run Some Keyword', 11), 'Some Keyword')
-
- def test_tab_char(self):
- self.assertEqual(get_keyword_at_pos('Run\tSome Keyword', 5), 'Some Keyword')
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/lib/robot_auto_completion.py b/lib/robot_auto_completion.py
new file mode 100644
index 0000000..74207bb
--- /dev/null
+++ b/lib/robot_auto_completion.py
@@ -0,0 +1,93 @@
+import os
+import re
+import sublime
+from robot_common import is_robot_file
+
+#-------------------------------------------------------------------------
+# Class to handle auto completion of variable names and list names.
+#-------------------------------------------------------------------------
+
+class Search(object):
+ def __init__(self, view, edit, plugin_dir):
+ self.view = view
+ self.edit = edit
+ self.plugin_dir = plugin_dir
+ self.window = sublime.active_window()
+ self.variable_pattern = re.compile('\s*\\$\\{\w+\\}')
+ self.list_pattern = re.compile('\s*@\\{\w+\\}')
+ self.known_variables = []
+ self.known_lists = []
+
+ self._search_within_folders(view.window().folders())
+
+ def _search_within_folders(self, folders):
+ for folder in folders:
+ #print 'searching folder for variables: ' + folder
+ for root, dirs, files in os.walk(folder):
+ for file_name in files:
+ if is_robot_file(file_name):
+ file_path = os.path.join(root, file_name)
+ #print 'searching file for variables: ' + file_path
+ self._search_within_file(file_path)
+
+ def _search_within_file(self, file_path):
+
+ try:
+ with open(file_path, 'rb') as openFile:
+
+ lines = openFile.readlines()
+ inside_variable_block = False
+
+ for line in lines:
+ # Any line that starts with '***' marks start of a new code block in Robot.
+ if line.startswith('***'):
+ # we know that all variables must follow *** Variables ***
+ inside_variable_block = line.startswith('*** Variables ***')
+ continue
+
+ if inside_variable_block:
+ self._search_within_line(line)
+
+ except IOError as e:
+ return
+
+ def _search_within_line(self, line):
+ match = self.variable_pattern.match(line)
+ if match:
+ variable_name = re.sub('[${}]', '', match.group(0).strip())
+ if variable_name not in self.known_variables:
+ self.known_variables.append(variable_name)
+
+ match = self.list_pattern.match(line)
+ if match:
+ list_name = re.sub('[@{}]', '', match.group(0).strip())
+ if list_name not in self.known_lists:
+ self.known_lists.append(list_name)
+
+ def auto_complete_variable(self):
+ # display a panel containing a list of known variables and let the user chose.
+ self.window.show_quick_panel(self.known_variables, self._on_user_selection_of_variable)
+
+ # replace the user typed ${{ with just ${
+ self._insert_text('${')
+ self.curPos = self.view.sel()[0].begin()
+
+ def auto_complete_list(self):
+ # display a panel containing a list of known lists and let the user chose.
+ self.window.show_quick_panel(self.known_lists, self._on_user_selection_of_list)
+
+ # replace the user typed @{{ with just @{
+ self._insert_text('@{')
+ self.curPos = self.view.sel()[0].begin()
+
+ def _on_user_selection_of_variable(self, index):
+ if index != -1:
+ self._insert_text(self.known_variables[index] + '} ')
+
+ def _on_user_selection_of_list(self, index):
+ if index != -1:
+ self._insert_text(self.known_lists[index] + '} ')
+
+ def _insert_text(self, text):
+ self.view.insert(self.edit, self.view.sel()[0].begin(), text)
+
diff --git a/lib/robot_common.py b/lib/robot_common.py
new file mode 100644
index 0000000..20d9317
--- /dev/null
+++ b/lib/robot_common.py
@@ -0,0 +1,110 @@
+import os
+import re
+import sublime
+
+from robot.api import TestCaseFile
+from robot.parsing.populators import FromFilePopulator
+
+views_to_center = {}
+
+#-------------------------------------------------------------------------------------------
+# This is a generic class that can be used to open a new window and display text in it.
+#-------------------------------------------------------------------------------------------
+class OutputWindow():
+ def __init__(self, window, plugin_dir, name):
+
+ self.console = window.new_file()
+ self.console.set_name(name)
+
+ self.console.set_scratch(True)
+ self.console.set_read_only(True)
+ self.console.set_syntax_file(os.path.join(plugin_dir, 'robot-output.tmLanguage'))
+
+ def append_text(self, output):
+
+ self.console.set_read_only(False)
+ edit = self.console.begin_edit()
+ self.console.insert(edit, self.console.size(), output)
+ self.console.end_edit(edit)
+ self.console.set_read_only(True)
+
+#--------------------------------------------------------------------------
+# This class represents an expanded test file
+#--------------------------------------------------------------------------
+class RobotTestCaseFile():
+ def __init__(self, view):
+ # get lines from the test suite
+ regions = view.split_by_newlines(sublime.Region(0, view.size()))
+ lines = [view.substr(region).encode('ascii', 'replace') + '\n' for region in regions]
+ self.file = TestCaseFile(source = view.file_name())
+ FromStringPopulator(self.file, lines).populate(self.file.source)
+
+#--------------------------------------------------------------------------
+#
+#--------------------------------------------------------------------------
+class FromStringPopulator(FromFilePopulator):
+ def __init__(self, datafile, lines):
+ super(FromStringPopulator, self).__init__(datafile)
+ self.lines = lines
+
+ def readlines(self):
+ return self.lines
+
+ def close(self):
+ pass
+
+ def _open(self, path):
+ return self
+
+#--------------------------------------------------------------------------
+# Commonly used function to check if the current file is a robot file.
+#--------------------------------------------------------------------------
+def is_robot_format(view):
+ return view.settings().get('syntax').endswith('robot.tmLanguage')
+
+def is_robot_file(file_name):
+ return file_name.endswith('.txt')
+
+#--------------------------------------------------------------------------
+# The line of text at the current cursor position in the active window.
+#--------------------------------------------------------------------------
+
+class LineAtCursor():
+ def __init__(self, view):
+ self.view = view
+ sel = view.sel()[0]
+ self.line = re.compile('\r|\n').split(view.substr(view.line(sel)))[0]
+ self.row, self.col = view.rowcol(sel.begin())
+
+ # gets the keyword from the line
+ def get_keyword(self):
+ return get_keyword_at_pos(self.line, self.col)
+
+def get_keyword_at_pos(line, col):
+ length = len(line)
+
+ if length == 0:
+ return None
+
+ # between spaces
+ if ((col >= length or line[col] == ' ' or line[col] == '\t')
+ and (col == 0 or line[col-1] == ' ' or line[col-1] == '\t')):
+ return None
+
+ # first look back until we find 2 spaces in a row, or reach the beginning
+ i = col - 1
+ while i >= 0:
+ if line[i] == '\t' or ((line[i - 1] == ' ' or line[i - 1] == '|') and line[i] == ' '):
+ break
+ i -= 1
+ begin = i + 1
+
+ # now look forward or until the end
+ i = col # previous included line[col]
+ while i < length:
+ if line[i] == '\t' or (line[i] == ' ' and len(line) > i and (line[i + 1] == ' ' or line[i + 1] == '|')):
+ break
+ i += 1
+ end = i
+
+ return line[begin:end]
diff --git a/lib/robot_definitions.py b/lib/robot_definitions.py
new file mode 100644
index 0000000..84d299e
--- /dev/null
+++ b/lib/robot_definitions.py
@@ -0,0 +1,65 @@
+import os
+import sublime
+import threading
+import stdlib_keywords
+
+from robot_scanner import Scanner
+from robot_common import is_robot_file, views_to_center
+
+#------------------------------------------------------
+#
+#------------------------------------------------------
+
+class GoToKeywordThread(threading.Thread):
+ def __init__(self, view, view_file, keyword, folders):
+ self.view = view
+ self.view_file = view_file
+ self.keyword = keyword
+ self.folders = folders
+ threading.Thread.__init__(self)
+
+ def run(self):
+ scanner = Scanner(self.view)
+ keywords = scanner.scan_file(self.view_file)
+
+ for folder in self.folders:
+ for root, dirs, files in os.walk(folder):
+ for f in files:
+ if is_robot_file(f) and f != '__init__.txt':
+ path = os.path.join(root, f)
+ scanner.scan_without_resources(path, keywords)
+
+ results = []
+ for bdd_prefix in ['given ', 'and ', 'when ', 'then ']:
+ if self.keyword.lower().startswith(bdd_prefix):
+ substr = self.keyword[len(bdd_prefix):]
+ results.extend(self.search_user_keywords(keywords, substr))
+ results.extend(stdlib_keywords.search_keywords(substr))
+
+ results.extend(self.search_user_keywords(keywords, self.keyword))
+ results.extend(stdlib_keywords.search_keywords(self.keyword))
+
+ sublime.set_timeout(lambda: self._select_keyword_and_go(self.view, results), 0)
+
+ def search_user_keywords(self, keywords, name):
+ lower_name = name.lower()
+ if not keywords.has_key(lower_name):
+ return []
+ return keywords[lower_name]
+
+ def _select_keyword_and_go(self, view, results):
+ def on_done(index):
+ if index == -1:
+ return
+ results[index].show_definition(view, views_to_center)
+
+ if len(results) == 1 and results[0].allow_unprompted_go_to():
+ results[0].show_definition(view, views_to_center)
+ return
+
+ result_strings = []
+ for kw in results:
+ strings = [kw.name]
+ strings.extend(kw.description)
+ result_strings.append(strings)
+ view.window().show_quick_panel(result_strings, on_done)
diff --git a/lib/robot_references.py b/lib/robot_references.py
new file mode 100644
index 0000000..502b5fc
--- /dev/null
+++ b/lib/robot_references.py
@@ -0,0 +1,142 @@
+import os
+import re
+import sublime
+import shutil
+import tempfile
+
+from robot_common import OutputWindow, LineAtCursor, is_robot_file, get_keyword_at_pos
+
+#------------------------------------------------------
+# Class that is used to find/replace keyword references.
+#------------------------------------------------------
+
+class FindReferencesService():
+ def __init__(self, view, edit, plugin_dir):
+ self.view = view
+ self.edit = edit
+ self.plugin_dir = plugin_dir
+ self.window = sublime.active_window()
+ self.results_to_display = []
+ self.references = []
+
+ def find(self):
+ keyword = LineAtCursor(self.view).get_keyword()
+
+ if not keyword:
+ sublime.error_message('No keyword detected')
+ return
+
+ self._search_within_folder(keyword, self._find_callback)
+ self.window.show_quick_panel(self.results_to_display, self._on_user_select, sublime.MONOSPACE_FONT)
+
+ def replace(self, edit, old_keyword, new_keyword):
+ self.output_window = OutputWindow(self.window, self.plugin_dir, '*Find/Replace References*')
+ if self.output_window is None:
+ sublime.error_message('Cannot open a window to display the output. The command quits. No replacings will be made.')
+ return
+
+ self.old_keyword = old_keyword
+ self.new_keyword = new_keyword
+ self.replacement_count = 0
+ self.previous_file_path = ''
+
+ self._display_find_and_replace_window_header(self.output_window)
+ self._search_within_folder(old_keyword, self._replace_callback)
+
+ if self.replacement_count > 0:
+ self.output_window.append_text('\nTotal of ' + str(self.replacement_count) + ' occurrences replaced')
+
+ def _search_within_folder(self, phrase, callback):
+ for folder in self.window.folders():
+ for root, dirs, files in os.walk(folder):
+ for file in files:
+ if is_robot_file(file):
+ self._search_within_file(root, file, phrase, callback)
+
+ def _search_within_file(self, root, file_name, phrase, callback):
+ file_path = os.path.join(root, file_name)
+ try:
+ with open(file_path, 'rb') as file:
+ lines = file.readlines()
+ line_number = 0
+ for a_line in lines:
+ line_number = line_number + 1
+ try:
+ if phrase in str(a_line):
+ # now we know that the phrase is inside the line, but is it really a keyword, let's see...
+ occurrance = get_keyword_at_pos(a_line, a_line.index(phrase) + 1)
+ if occurrance == phrase:
+ reference = ReferencedLine(a_line.strip(), str(file_name), file_path, line_number)
+ callback(reference)
+
+ except Exception as exp:
+ print('Error in file: ' + str(file_path) + '(' + str(line_number) + '): ' + str(exp))
+
+ except IOError as e:
+ return
+
+ def _find_callback(self, reference):
+ self.references.append(reference)
+ self.results_to_display.append(reference.to_display())
+
+ def _on_user_select(self, index):
+ if index != -1:
+ new_view = self.window.open_file(self.references[index].link(), sublime.ENCODED_POSITION)
+ self.window.focus_view(new_view)
+ pt = new_view.text_point(self.references[index].line_number - 1, 0)
+ new_view.sel().clear()
+ new_view.sel().add(sublime.Region(pt))
+ new_view.show(pt)
+
+ def _display_find_and_replace_window_header(self, window):
+ title = 'Replacing "' + self.old_keyword + '" with "' + self.new_keyword +'"\n'
+ window.append_text('-' * (len(title) + 8) + '\n')
+ window.append_text(' ' * 4 + title)
+ window.append_text('-' * (len(title) + 8) + '\n\n\n')
+
+ def _replace_callback(self, reference):
+ # if this is reference in a new file...
+ if self.previous_file_path != reference.file_path:
+ if self.previous_file_path != '':
+ self._replace_all_references_in_file(self.previous_file_path)
+ self.output_window.append_text('In file "' + str(reference.file_path) + '":\n')
+
+ self.previous_file_path = reference.file_path
+ self.output_window.append_text(' Replacing the keyword in line (' + str(reference.line_number) + '): ' + reference.line_text.strip() + '\n\n')
+ self.replacement_count = self.replacement_count + 1
+
+ def _replace_all_references_in_file(self, file_path):
+ # create a temporary file
+ fh, abs_path = tempfile.mkstemp()
+ new_file = open(abs_path, 'w')
+ old_file = open(file_path)
+ for line in old_file:
+ new_file.write(line.replace(self.old_keyword, self.new_keyword))
+
+ # close temp file
+ new_file.close()
+ os.close(fh)
+ old_file.close()
+
+ # remove original file
+ os.remove(file_path)
+
+ # move new file
+ shutil.move(abs_path, file_path)
+
+#----------------------------------------------------------------
+# Represents a single reference to a phrase (keyword/variable)
+#----------------------------------------------------------------
+
+class ReferencedLine:
+ def __init__(self, line_text, file_name, file_path, line_number):
+ self.line_text = line_text
+ self.file_name = file_name
+ self.file_path = file_path
+ self.line_number = line_number
+
+ def to_display(self):
+ return self.file_path + '(' + str(self.line_number) + '): '+ self.line_text
+
+ def link(self):
+ return self.file_path + ':' + str(self.line_number)
diff --git a/lib/robot_run.py b/lib/robot_run.py
new file mode 100644
index 0000000..90e91bf
--- /dev/null
+++ b/lib/robot_run.py
@@ -0,0 +1,270 @@
+import os
+import re
+import sublime
+import threading
+import subprocess
+import functools
+import json
+import webbrowser
+import shutil
+
+from robot_common import OutputWindow
+
+#----------------------------------------------------------
+# This class handles running a single test suite
+#----------------------------------------------------------
+class RobotTestSuite(object):
+
+ def __init__(self, view, plugin_dir):
+ self.view = view
+ self.plugin_dir = plugin_dir
+
+ def execute(self):
+ test = Test(self.view, self.plugin_dir)
+
+ if not test.initialized:
+ return False
+
+ test.run_test()
+ return True
+
+#----------------------------------------------------------
+# This class handles running a single test case
+#----------------------------------------------------------
+class RobotTestCase(object):
+
+ def __init__(self, view, plugin_dir):
+ self.view = view
+ self.plugin_dir = plugin_dir
+
+ def execute(self):
+ test = Test(self.view, self.plugin_dir)
+
+ if not test.initialized:
+ return False
+
+ #TODO: this returns the keyword at cursor position, but we need to get the keyword at mouse position.
+ sel = self.view.sel()[0]
+ test_case_name = re.compile('\r|\n').split(self.view.substr(self.view.line(sel)))[0]
+
+ #TODO: We can do few enhancements to this....
+ # 1. Make sure the selected test case actually appears under ***Test Cases*** section.
+ # 2. Even if the user clicks on a keyword inside a test case, execute the test case to which it belongs.
+ if (len(test_case_name) == 0) or (test_case_name[0] == ' ') or (test_case_name[0] == '\t'):
+ sublime.error_message('Please place cursor on a test case')
+ return
+
+ test_case_name = test_case_name.replace(' ', '').replace('\t', '')
+ print ('Test case name = ' + test_case_name)
+
+ test.run_test('--test ' + test_case_name)
+
+ return True
+
+#----------------------------------------------------------
+# This class is used to execute tests.
+#----------------------------------------------------------
+class Test():
+
+ def __init__(self, view, plugin_dir):
+ self.initialized = False
+ self.view = view
+ self.plugin_dir = plugin_dir
+ self.robot_root_folder = view.window().folders()[0]
+
+ if not view.file_name():
+ sublime.error_message('Please save the buffer to a file first.')
+ return
+
+ # set default values for the run parameters.
+ self.outputdir = 'TestResults'
+ self.testsuites = 'testsuites'
+ variables = []
+ tags_to_exclude = []
+ tags_to_include = []
+
+ # load settings(testsuites name, output directory, variables, tags) from settings file.
+ settings_file_name = os.path.join(self.robot_root_folder, 'robot.sublime-build')
+ source_settings_file_name = os.path.join(plugin_dir, 'robot.sublime-build')
+ print ('Reading the settings from: ' + settings_file_name)
+
+ if os.path.isfile(settings_file_name):
+ try:
+ json_data = open(settings_file_name)
+ data = json.load(json_data)
+ json_data.close()
+
+ print ('JSON loaded. Now reading the settings...')
+ if len(data['testsuites']) > 0:
+ self.testsuites = data['testsuites']
+
+ if len(data['outputdir']) > 0:
+ self.outputdir = data['outputdir']
+
+ if len(data['variables']) > 0:
+ variables = data['variables']
+
+ if len(data['tags_to_exclude']) > 0:
+ tags_to_exclude = data['tags_to_exclude']
+
+ if len(data['tags_to_include']) > 0:
+ tags_to_include = data['tags_to_include']
+
+ except:
+ user_decided_to_discard = sublime.ok_cancel_dialog('Error reading: (' + settings_file_name + '). Do you want to discard the existing file and create a new settings file?', 'Discard')
+ if user_decided_to_discard:
+ shutil.copyfile(source_settings_file_name, settings_file_name)
+ sublime.active_window().open_file(settings_file_name)
+ return
+
+ else:
+ print 'Settings file (' + settings_file_name + ') not found. Copying the default from plugin directory.'
+ sublime.error_message('Test runner settings file is not found. Please press ok to create a new settings file and update according to your requirements')
+ shutil.copyfile(source_settings_file_name, settings_file_name)
+ sublime.active_window().open_file(settings_file_name)
+ return
+
+ # make sure test suites and results folders do not contain white-spaces inside.
+ whitespace_pattern = re.compile('.*\s')
+ if whitespace_pattern.match(self.testsuites):
+ sublime.error_message('Testsuites folder: "' + self.testsuites + '" contains white-spaces!')
+ return
+
+ if whitespace_pattern.match(self.outputdir):
+ sublime.error_message('Results folder: "' + self.outputdir + '" contains white-spaces!')
+ return
+
+ # append variables together so that they can be appended to the pybot command
+ self.variable_line = ''
+ for variable in variables:
+ if whitespace_pattern.match(variable):
+ sublime.error_message('Variable: "' + variable + '" contains white-spaces and is not allowed!')
+ return
+ self.variable_line += '--variable ' + variable + ' '
+
+ # append exclude tags together so that they can be appended to the pybot command
+ self.exclude_tags = ''
+ for exclude_tag in tags_to_exclude:
+ if whitespace_pattern.match(exclude_tag):
+ sublime.error_message('Tag: "' + exclude_tag + '" contains white-spaces and is not allowed!')
+ return
+ self.exclude_tags += '--exclude ' + exclude_tag + ' '
+
+ # append include tags together so that they can be appended to the pybot command
+ self.include_tags = ''
+ for include_tag in tags_to_include:
+ if whitespace_pattern.match(include_tag):
+ sublime.error_message('Tag: "' + include_tag + '" contains white-spaces and is not allowed!')
+ return
+ self.include_tags += '--include ' + include_tag + ' '
+
+ # find the suite name
+ test_suite_path, test_suite_file_name = os.path.split(view.file_name())
+ self.test_suite_name = os.path.splitext(test_suite_file_name)[0]
+
+ print ('Test suite path = ' + test_suite_path)
+ print ('Test suites = ' + self.testsuites)
+ test_suite_path = os.path.relpath(test_suite_path, os.path.join(self.robot_root_folder, self.testsuites)).replace('\\', '.')
+
+ if not (test_suite_path == '.'):
+ self.test_suite_name = test_suite_path + '.' + self.test_suite_name
+
+ self.test_suite_name = self.test_suite_name.replace(' ', '')
+ print ('Test suite name = ' + self.test_suite_name)
+ self.initialized = True
+
+ def run_test(self, filter = ''):
+ if not self.initialized:
+ return
+
+ output_window = OutputWindow(self.view.window(), self.plugin_dir, '*Output*')
+
+ def _display_text_in_output_window(output):
+ if output is not None:
+ output_window.append_text(output)
+
+ self._open_thread_to_execute_pybot(
+ 'pybot --outputdir ' + self.outputdir + ' ' +
+ self.variable_line + self.exclude_tags + self.include_tags +
+ '--suite ' + self.test_suite_name + ' ' +
+ filter + ' ' + self.testsuites,
+ _display_text_in_output_window,
+ self.robot_root_folder,
+ self.outputdir
+ )
+
+ def _open_thread_to_execute_pybot(self, command, callback, working_dir, outputdir):
+
+ thread = threading.Thread(
+ target = self._execute_pybot,
+ kwargs = {
+ 'command': command,
+ 'callback': callback,
+ 'working_dir': working_dir,
+ 'outputdir': outputdir
+ }
+ )
+
+ thread.start()
+
+ def _execute_pybot(self, command, callback, working_dir, outputdir, **kwargs):
+ startupinfo = None
+ return_code = None
+ if os.name == 'nt':
+ startupinfo = subprocess.STARTUPINFO()
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
+
+ try:
+
+ # display the pybot command that we execute
+ main_thread(callback, command + '\n\n')
+
+ # start pybot command
+ proc = subprocess.Popen(
+ command,
+ stdin = subprocess.PIPE,
+ universal_newlines = True,
+ stdout = subprocess.PIPE,
+ stderr = subprocess.STDOUT,
+ shell = True,
+ cwd = working_dir,
+ startupinfo = startupinfo
+ )
+
+ # collect input while the pybot command is in progress
+ while return_code is None:
+ return_code = proc.poll()
+
+ if return_code is None or return_code == 0:
+ output = True
+ while output:
+ output = proc.stdout.readline()
+ main_thread(callback, output, **kwargs)
+
+ except subprocess.CalledProcessError as e:
+
+ main_thread(callback, e.returncode)
+
+ except OSError as e:
+
+ if e.errno == 2:
+ sublime.message_dialog('Command not found\n\nCommand is: %s' % command)
+ else:
+ raise e
+
+ if return_code == 0:
+ main_thread(callback, '\nTest execution is complete and all tests passed!', **kwargs)
+ else:
+ main_thread(callback, '\nTest execution is complete, but there are test failures!', **kwargs)
+
+ output_file_name = os.path.join(working_dir, outputdir, 'log.html')
+ print 'output file: ' + output_file_name
+
+ if os.path.isfile(output_file_name):
+ webbrowser.open_new('file://' + output_file_name)
+
+#-------------------------------------------------------------------------
+# A function that executes a callback function on the main thread.
+#-------------------------------------------------------------------------
+def main_thread(callback, *args, **kwargs):
+ sublime.set_timeout(functools.partial(callback, *args, **kwargs), 0)
diff --git a/lib/robot_scanner.py b/lib/robot_scanner.py
index 212aced..3991a8c 100644
--- a/lib/robot_scanner.py
+++ b/lib/robot_scanner.py
@@ -1,17 +1,16 @@
import os
import re
+import sublime
+
from copy import copy
from collections import deque
from time import time
-import sublime
-
from robot.api import TestCaseFile, ResourceFile
from robot.errors import DataError
from scanner_cache import ScannerCache
-from string_populator import populate_from_lines
-
+from robot_common import FromStringPopulator
scanner_cache = ScannerCache()
@@ -19,38 +18,16 @@
detect_robot_regex = '\*+\s*(settings?|metadata|(user )?keywords?|test ?cases?|variables?)'
-class WrappedKeyword:
- def __init__(self, data_file, keyword, file_path):
- self.keyword = keyword
- self.name = data_file.name + '.' + keyword.name
- self.file_path = file_path
- self.description = []
- args = ', '.join(keyword.args.value)
- if args:
- self.description.append(args)
- if keyword.doc.value:
- self.description.append(keyword.doc.value)
- self.description.append(file_path)
-
- def show_definition(self, view, views_to_center):
- source_path = self.keyword.source
- new_view = view.window().open_file("%s:%d" % (source_path, self.keyword.linenumber), sublime.ENCODED_POSITION)
- new_view.show_at_center(new_view.text_point(self.keyword.linenumber, 0))
- if new_view.is_loading():
- views_to_center[new_view.id()] = self.keyword.linenumber
-
- def __eq__(self, other):
- return isinstance(other, WrappedKeyword) and self.file_path == other.file_path
-
- def allow_unprompted_go_to(self):
- return True
-
+#-------------------------------------------------------------------------------
+# This class recursively scans a robot test suite file for all the keywords.
+#-------------------------------------------------------------------------------
class Scanner(object):
def __init__(self, view):
self.view = view
+ # main method, returns all the keywords.
def scan_file(self, data_file):
self.start_time = time()
self.start_path = data_file.directory
@@ -59,6 +36,30 @@ def scan_file(self, data_file):
self.__scan_file(keywords, data_file, deque())
return keywords
+ def scan_without_resources(self, file_path, keywords):
+ if file_path in self.scanned_files:
+ return
+
+ try:
+ with open(file_path, 'rb') as f:
+ lines = f.readlines()
+ except IOError as e:
+ return
+
+ cached, stored_hash = scanner_cache.get_cached_data(file_path, lines)
+ if cached:
+ self._scan_keywords(cached, keywords)
+ else:
+ try:
+ for line in lines:
+ if re.search(detect_robot_regex, line, re.IGNORECASE) != None:
+ data_file = self._populate_from_lines(lines, file_path)
+ scanner_cache.put_data(file_path, data_file, stored_hash)
+ self._scan_keywords(data_file, keywords)
+ break
+ except DataError as de:
+ pass
+
def __scan_file(self, keywords, data_file, import_history):
if time() - self.start_time > SCAN_TIMEOUT:
sublime.set_timeout(lambda: self.view.set_status('scan_error', 'scanning timeout exceeded'), 0)
@@ -86,9 +87,9 @@ def __scan_file(self, keywords, data_file, import_history):
except DataError as de:
print 'error reading resource:', resource_path
- self.scan_keywords(data_file, keywords)
+ self._scan_keywords(data_file, keywords)
- def scan_keywords(self, data_file, keywords):
+ def _scan_keywords(self, data_file, keywords):
for keyword in data_file.keyword_table:
lower_name = keyword.name.lower()
if not keywords.has_key(lower_name):
@@ -98,26 +99,39 @@ def scan_keywords(self, data_file, keywords):
continue
keywords[lower_name].append(wrapped)
- def scan_without_resources(self, file_path, keywords):
- if file_path in self.scanned_files:
- return
+ def _populate_from_lines(self, lines, file_path):
+ data_file = TestCaseFile(source = file_path)
+ FromStringPopulator(data_file, lines).populate(file_path)
+ return data_file
- try:
- with open(file_path, 'rb') as f:
- lines = f.readlines()
- except IOError as e:
- return
+#-------------------------------------------------------------------------------
+# Information about a single keyword
+#-------------------------------------------------------------------------------
+
+class WrappedKeyword:
+ def __init__(self, data_file, keyword, file_path):
+ self.keyword = keyword
+ self.name = data_file.name + '.' + keyword.name
+ self.file_path = file_path
+ self.description = []
+ args = ', '.join(keyword.args.value)
+ if args:
+ self.description.append(args)
+ if keyword.doc.value:
+ self.description.append(keyword.doc.value)
+ self.description.append(file_path)
+
+ # display a keyword definition in a popup window
+ def show_definition(self, view, views_to_center):
+ source_path = self.keyword.source
+ new_view = view.window().open_file('%s:%d' % (source_path, self.keyword.linenumber), sublime.ENCODED_POSITION)
+ new_view.show_at_center(new_view.text_point(self.keyword.linenumber, 0))
+ if new_view.is_loading():
+ views_to_center[new_view.id()] = self.keyword.linenumber
+
+ def __eq__(self, other):
+ return isinstance(other, WrappedKeyword) and self.file_path == other.file_path
+
+ def allow_unprompted_go_to(self):
+ return True
- cached, stored_hash = scanner_cache.get_cached_data(file_path, lines)
- if cached:
- self.scan_keywords(cached, keywords)
- else:
- try:
- for line in lines:
- if re.search(detect_robot_regex, line, re.IGNORECASE) != None:
- data_file = populate_from_lines(lines, file_path)
- scanner_cache.put_data(file_path, data_file, stored_hash)
- self.scan_keywords(data_file, keywords)
- break
- except DataError as de:
- pass
diff --git a/lib/string_populator.py b/lib/string_populator.py
deleted file mode 100644
index fd6a531..0000000
--- a/lib/string_populator.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import sublime
-
-from robot.api import TestCaseFile
-from robot.parsing.populators import FromFilePopulator
-
-class FromStringPopulator(FromFilePopulator):
- def __init__(self, datafile, lines):
- super(FromStringPopulator, self).__init__(datafile)
- self.lines = lines
-
- def readlines(self):
- return self.lines
-
- def close(self):
- pass
-
- def _open(self, path):
- return self
-
-def populate_testcase_file(view):
- regions = view.split_by_newlines(sublime.Region(0, view.size()))
- lines = [view.substr(region).encode('ascii', 'replace') + '\n' for region in regions]
- test_case_file = TestCaseFile(source=view.file_name())
- FromStringPopulator(test_case_file, lines).populate(test_case_file.source)
- return test_case_file
-
-def populate_from_lines(lines, file_path):
- data_file = TestCaseFile(source=file_path)
- FromStringPopulator(data_file, lines).populate(file_path)
- return data_file
diff --git a/package-metadata.json b/package-metadata.json
new file mode 100644
index 0000000..b1a320f
--- /dev/null
+++ b/package-metadata.json
@@ -0,0 +1 @@
+{"url": "https://github.com/shellderp/sublime-robot-plugin", "version": "2013.08.23.01.36.39", "description": "Provides basic tools for working with Robot Framework text files in Sublime Text 2."}
\ No newline at end of file
diff --git a/plugin.py b/plugin.py
index 6eac4c6..bc5a698 100644
--- a/plugin.py
+++ b/plugin.py
@@ -1,5 +1,10 @@
+#------------------------------------------------------
+# Library imports and initializations
+#------------------------------------------------------
+
# setup pythonpath to include lib directory before other imports
import os, sys
+
lib_path = os.path.normpath(os.path.join(os.getcwd(), 'lib'))
if lib_path not in sys.path:
sys.path.append(lib_path)
@@ -10,137 +15,267 @@
# only available when the plugin is being loaded
plugin_dir = os.getcwd()
-import threading
import re
+import sublime
+import sublime_plugin
-import sublime, sublime_plugin
-
-from keyword_parse import get_keyword_at_pos
-from string_populator import populate_testcase_file
from robot_scanner import Scanner, detect_robot_regex
-import stdlib_keywords
-
+from robot_common import OutputWindow, RobotTestCaseFile, LineAtCursor, is_robot_format, is_robot_file, views_to_center
+from robot_definitions import GoToKeywordThread
+from robot_references import FindReferencesService
-views_to_center = {}
+import robot_run
+import robot_auto_completion
+import stdlib_keywords
stdlib_keywords.load(plugin_dir)
-def is_robot_format(view):
- return view.settings().get('syntax').endswith('robot.tmLanguage')
+#====================================================================================================
+# Classes used for finding the definition of a keyword.
+# Note: See lib/robot_definitions.py for detailed implementation.
+#====================================================================================================
+#------------------------------------------------------
+# Sublime context menu command: Go to definition
+#------------------------------------------------------
+
+class RobotGoToKeywordCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ view = self.view
-def select_keyword_and_go(view, results):
- def on_done(index):
- if index == -1:
+ if not is_robot_format(view):
return
- results[index].show_definition(view, views_to_center)
- if len(results) == 1 and results[0].allow_unprompted_go_to():
- results[0].show_definition(view, views_to_center)
- return
+ file_path = view.file_name()
+ if not file_path:
+ sublime.error_message('Please save the buffer to a file first.')
+ return
+ path, file_name = os.path.split(file_path)
- result_strings = []
- for kw in results:
- strings = [kw.name]
- strings.extend(kw.description)
- result_strings.append(strings)
- view.window().show_quick_panel(result_strings, on_done)
+ line_at_cursor = LineAtCursor(view)
+ keyword = line_at_cursor.get_keyword()
+ line = line_at_cursor.line
+ if not keyword:
+ return
-class GoToKeywordThread(threading.Thread):
- def __init__(self, view, view_file, keyword, folders):
- self.view = view
- self.view_file = view_file
- self.keyword = keyword
- self.folders = folders
- threading.Thread.__init__(self)
+ if line.strip().startswith('Resource'):
+ resource = line[line.find('Resource') + 8:].strip().replace('${CURDIR}', path)
+ resource_path = os.path.join(path, resource)
+ view.window().open_file(resource_path)
+ return
- def run(self):
- scanner = Scanner(self.view)
- keywords = scanner.scan_file(self.view_file)
+ view_file = RobotTestCaseFile(self.view).file
- for folder in self.folders:
- for root, dirs, files in os.walk(folder):
- for f in files:
- if f.endswith('.txt') and f != '__init__.txt':
- path = os.path.join(root, f)
- scanner.scan_without_resources(path, keywords)
+ # must be run on main thread
+ folders = view.window().folders()
+ GoToKeywordThread(view, view_file, keyword, folders).start()
- results = []
- for bdd_prefix in ['given ', 'and ', 'when ', 'then ']:
- if self.keyword.lower().startswith(bdd_prefix):
- substr = self.keyword[len(bdd_prefix):]
- results.extend(self.search_user_keywords(keywords, substr))
- results.extend(stdlib_keywords.search_keywords(substr))
+#====================================================================================================
+# Classes used for running robot tests.
+# Note: See lib/robot_run.py for detailed implementation.
+#====================================================================================================
- results.extend(self.search_user_keywords(keywords, self.keyword))
- results.extend(stdlib_keywords.search_keywords(self.keyword))
+#----------------------------------------------------------
+# Sublime context menu command: Run test
+#----------------------------------------------------------
+class RobotRunTestCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ if not is_robot_format(self.view):
+ return
- sublime.set_timeout(lambda: select_keyword_and_go(self.view, results), 0)
+ test_case = robot_run.RobotTestCase(self.view, plugin_dir)
+ test_case.execute()
- def search_user_keywords(self, keywords, name):
- lower_name = name.lower()
- if not keywords.has_key(lower_name):
- return []
- return keywords[lower_name]
+#----------------------------------------------------------
+# Sublime context menu command: Run test suite
+#----------------------------------------------------------
+class RobotRunTestSuiteCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ if not is_robot_format(self.view):
+ return
+ test_suite = robot_run.RobotTestSuite(self.view, plugin_dir)
+ test_suite.execute()
-class RobotGoToKeywordCommand(sublime_plugin.TextCommand):
+#----------------------------------------------------------
+# Sublime context menu command: Run...
+#----------------------------------------------------------
+class RobotRunPanelCommand(sublime_plugin.TextCommand):
def run(self, edit):
- view = self.view
-
- if not is_robot_format(view):
+ if not is_robot_format(self.view):
return
- sel = view.sel()[0]
- line = re.compile('\r|\n').split(view.substr(view.line(sel)))[0]
- row, col = view.rowcol(sel.begin())
+ file_path = self.view.file_name()
- file_path = view.file_name()
if not file_path:
sublime.error_message('Please save the buffer to a file first.')
return
+
path, file_name = os.path.split(file_path)
- if line.strip().startswith('Resource'):
- resource = line[line.find('Resource') + 8:].strip().replace('${CURDIR}', path)
- resource_path = os.path.join(path, resource)
- view.window().open_file(resource_path)
- return
+ sublime.error_message('Run panel is not yet implemented')
- keyword = get_keyword_at_pos(line, col)
- if not keyword:
- return
+#------------------------------------------------------------------------------------
+# Sublime menu command: Preferences -> Package Settings -> Arigato -> Run options
+#------------------------------------------------------------------------------------
+class RobotRunOptionsCommand(sublime_plugin.WindowCommand):
+ def run(self):
- view_file = populate_testcase_file(self.view)
- # must be run on main thread
- folders = view.window().folders()
- GoToKeywordThread(view, view_file, keyword, folders).start()
+ current_folder = sublime.active_window().folders()[0]
+ sublime.active_window().open_file(os.path.join(current_folder, 'robot.sublime-build'))
+#====================================================================================================
+# Classes used for auto completion.
+# Note: See lib/robot_auto_completion.py for detailed implementation.
+#====================================================================================================
-class AutoSyntaxHighlight(sublime_plugin.EventListener):
- def autodetect(self, view):
- # file name can be None if it's a find result view that is restored on startup
- if (view.file_name() != None and view.file_name().endswith('.txt') and
- view.find(detect_robot_regex, 0, sublime.IGNORECASE) != None):
+#----------------------------------------------------------
+# Mapped key: ${{ For auto completion of variable names.
+#----------------------------------------------------------
+
+class RobotCompleteVariableCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ search = robot_auto_completion.Search(self.view, edit, plugin_dir)
+ search.auto_complete_variable()
+
+#------------------------------------------------------
+# Mapped key: @{{ For auto completion of list names.
+#------------------------------------------------------
+
+class RobotCompleteListCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ search = robot_auto_completion.Search(self.view, edit, plugin_dir)
+ search.auto_complete_list()
+
+#====================================================================================================
+# Event listeners
+#====================================================================================================
- view.set_syntax_file(os.path.join(plugin_dir, "robot.tmLanguage"))
+#------------------------------------------------------
+# Highlight robot framework syntax.
+#------------------------------------------------------
+class AutoSyntaxHighlight(sublime_plugin.EventListener):
def on_load(self, view):
if view.id() in views_to_center:
view.show_at_center(view.text_point(views_to_center[view.id()], 0))
del views_to_center[view.id()]
- self.autodetect(view)
+ self._autodetect(view)
def on_post_save(self, view):
- self.autodetect(view)
+ self._autodetect(view)
+
+ def _autodetect(self, view):
+ # file name can be None if it's a find result view that is restored on startup
+ if (view.file_name() != None and is_robot_file(view.file_name()) and
+ view.find(detect_robot_regex, 0, sublime.IGNORECASE) != None):
+
+ view.set_syntax_file(os.path.join(plugin_dir, 'robot.tmLanguage'))
+#------------------------------------------------------
+# Auto completion of keywords.
+#------------------------------------------------------
class AutoComplete(sublime_plugin.EventListener):
def on_query_completions(self, view, prefix, locations):
if is_robot_format(view):
- view_file = populate_testcase_file(view)
+ view_file = RobotTestCaseFile(view).file
keywords = Scanner(view).scan_file(view_file)
lower_prefix = prefix.lower()
user_keywords = [(kw[0].keyword.name, kw[0].keyword.name) for kw in keywords.itervalues()
if kw[0].keyword.name.lower().startswith(lower_prefix)]
return user_keywords
+
+#====================================================================================================
+# Classes used for find/replace references.
+# Note: See lib/robot_references.py for detailed implementation.
+#====================================================================================================
+
+#------------------------------------------------------
+# # Sublime context menu command: Find references
+#------------------------------------------------------
+
+class RobotFindReferencesCommand(sublime_plugin.TextCommand):
+ def run(self, edit):
+ if not is_robot_format(self.view):
+ return
+
+ references = FindReferencesService(self.view, edit, plugin_dir)
+ references.find()
+
+#-----------------------------------------------------------
+# Sublime context menu command: Prompt Replace references
+#-----------------------------------------------------------
+
+class PromptRobotReplaceReferencesCommand(sublime_plugin.WindowCommand):
+ def run(self):
+ view = sublime.active_window().active_view()
+
+ if not is_robot_format(view):
+ return
+
+ self.currentKeyword = LineAtCursor(view).get_keyword()
+ self.window.show_input_panel('Replace "' + self.currentKeyword + '" with: ', self.currentKeyword, self._on_done, None, None)
+ pass
+
+ def _on_done(self, text):
+ try:
+ if self.window.active_view():
+ self.window.active_view().run_command('robot_replace_references', {'old_keyword': self.currentKeyword, 'new_keyword': text} )
+ except ValueError:
+ pass
+
+#------------------------------------------------------
+# Sublime context menu command: Replace references
+#------------------------------------------------------
+
+class RobotReplaceReferencesCommand(sublime_plugin.TextCommand):
+ def run(self, edit, old_keyword, new_keyword):
+ references = FindReferencesService(self.view, edit, plugin_dir)
+ references.replace(edit, old_keyword, new_keyword)
+
+#====================================================================================================
+# Experimental Stuff...
+#====================================================================================================
+
+#-------------------------------------------------------------------------------
+# TODO: (POC) Add mouse event listener to capture the mouse cursor position.
+#-------------------------------------------------------------------------------
+
+class MouseEventListener(sublime_plugin.EventListener):
+ #If we add the callback names to the list of all callbacks, Sublime
+ #Text will automatically search for them in future imported classes.
+ #You don't actually *need* to inherit from MouseEventListener, but
+ #doing so forces you to import this file and therefore forces Sublime
+ #to add these to its callback list.
+ sublime_plugin.all_callbacks.setdefault('on_pre_mouse_down', [])
+ sublime_plugin.all_callbacks.setdefault('on_post_mouse_down', [])
+
+class DragSelectCallbackCommand(sublime_plugin.TextCommand):
+ def run_(self, args):
+ for c in sublime_plugin.all_callbacks.setdefault('on_pre_mouse_down',[]):
+ c.on_pre_mouse_down(args)
+
+ #We have to make a copy of the selection, otherwise we'll just have
+ #a *reference* to the selection which is useless if we're trying to
+ #roll back to a previous one. A RegionSet doesn't support slicing so
+ #we have a comprehension instead.
+ old_sel = [r for r in self.view.sel()]
+
+ #Only send the event so we don't do an extend or subtract or
+ #whatever. We want the only selection to be where they clicked.
+ self.view.run_command('drag_select', {'event': args['event']})
+ new_sel = self.view.sel()
+ click_point = new_sel[0].a
+
+ #Restore the old selection so when we call drag_select it will
+ #behave normally.
+ new_sel.clear()
+ map(new_sel.add, old_sel)
+
+ #This is the 'real' drag_select that alters the selection for real.
+ self.view.run_command('drag_select', args)
+
+ for c in sublime_plugin.all_callbacks.setdefault('on_post_mouse_down',[]):
+ c.on_post_mouse_down(click_point)
+
diff --git a/robot-output.tmLanguage b/robot-output.tmLanguage
new file mode 100644
index 0000000..22039e9
--- /dev/null
+++ b/robot-output.tmLanguage
@@ -0,0 +1,61 @@
+
+
+
+
+ comment
+ Robot Framework syntax highlighting for output files.
+ fileTypes
+
+ txt
+
+ name
+ Robot framework output
+ patterns
+
+
+ match
+ \b(FAIL)\b
+ name
+ invalid.illegal
+
+
+ match
+ \b(PASS)\b
+ name
+ string.robot.header
+
+
+ match
+ ==
+ name
+ comment
+
+
+ match
+ (Output:|Log:|Report:)
+ name
+ constant.numeric.robot
+
+
+ begin
+ Test execution is complete and all tests passed
+ end
+ $
+ name
+ string.robot.header
+
+
+ begin
+ Test execution is complete, but there are test failures
+ end
+ $
+ name
+ keyword
+
+
+ scopeName
+ text.robot
+ uuid
+ 8728e0fe-14c6-4374-acde-da1857d0a378
+
+
diff --git a/robot.sublime-build b/robot.sublime-build
index 7102aed..4437567 100644
--- a/robot.sublime-build
+++ b/robot.sublime-build
@@ -1,3 +1,15 @@
{
- "cmd": ["/usr/local/bin/pybot", "$file"]
+ "testsuites": "testsuites"
+ , "outputdir": "TestResults\\TestResults-gc"
+ , "variables":
+ [
+ "os_browser:gc",
+ "environment_name:cp"
+ ]
+ , "tags_to_exclude":
+ [
+ ]
+ , "tags_to_include":
+ [
+ ]
}
diff --git a/robot.tmLanguage b/robot.tmLanguage
index 638b51d..d608c47 100644
--- a/robot.tmLanguage
+++ b/robot.tmLanguage
@@ -4,12 +4,13 @@
comment
- Robot Framework syntax highlighting for txt files.
+ Robot Framework syntax highlighting for .txt and .rob files.
fileTypes
txt
-
+ rob
+
keyEquivalent
^~R
name