From 366fbb0f0d8952c868066a5c5e7b0816e6b6ab9d Mon Sep 17 00:00:00 2001 From: Chandima Prematillake Date: Fri, 30 May 2014 07:36:12 -0400 Subject: [PATCH] Moved all the changes --- Arigato.pyproj | 259 +++++++++++++++ Arigato.sln | 29 ++ Context.sublime-menu | 116 +++++++ Default (Windows).sublime-keymap | 4 +- Main.sublime-menu | 4 + Snippets/arguments.sublime-snippet | 11 + Snippets/documentation.sublime-snippet | 11 + Snippets/forloop.sublime-snippet | 12 + Snippets/forloopinrange.sublime-snippet | 11 + Snippets/heading-keywords.sublime-snippet | 11 + Snippets/heading-settings.sublime-snippet | 11 + Snippets/heading-testcases.sublime-snippet | 11 + Snippets/heading-variables.sublime-snippet | 11 + Snippets/library-json.sublime-snippet | 11 + .../library-operatingsystem.sublime-snippet | 11 + .../library-selenium2library.sublime-snippet | 11 + Snippets/library-xml.sublime-snippet | 11 + lib/keyword_parse.py | 69 ---- lib/robot_auto_completion.py | 93 ++++++ lib/robot_common.py | 110 +++++++ lib/robot_definitions.py | 65 ++++ lib/robot_references.py | 142 +++++++++ lib/robot_run.py | 270 ++++++++++++++++ lib/robot_scanner.py | 120 +++---- lib/string_populator.py | 30 -- package-metadata.json | 1 + plugin.py | 299 +++++++++++++----- robot-output.tmLanguage | 61 ++++ robot.sublime-build | 14 +- robot.tmLanguage | 5 +- 30 files changed, 1586 insertions(+), 238 deletions(-) create mode 100644 Arigato.pyproj create mode 100644 Arigato.sln create mode 100644 Context.sublime-menu create mode 100644 Snippets/arguments.sublime-snippet create mode 100644 Snippets/documentation.sublime-snippet create mode 100644 Snippets/forloop.sublime-snippet create mode 100644 Snippets/forloopinrange.sublime-snippet create mode 100644 Snippets/heading-keywords.sublime-snippet create mode 100644 Snippets/heading-settings.sublime-snippet create mode 100644 Snippets/heading-testcases.sublime-snippet create mode 100644 Snippets/heading-variables.sublime-snippet create mode 100644 Snippets/library-json.sublime-snippet create mode 100644 Snippets/library-operatingsystem.sublime-snippet create mode 100644 Snippets/library-selenium2library.sublime-snippet create mode 100644 Snippets/library-xml.sublime-snippet delete mode 100644 lib/keyword_parse.py create mode 100644 lib/robot_auto_completion.py create mode 100644 lib/robot_common.py create mode 100644 lib/robot_definitions.py create mode 100644 lib/robot_references.py create mode 100644 lib/robot_run.py delete mode 100644 lib/string_populator.py create mode 100644 package-metadata.json create mode 100644 robot-output.tmLanguage 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