diff --git a/HISTORY.rst b/HISTORY.rst index d1d1681..75266c6 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -4,6 +4,35 @@ Release History --------------- +Release 3.1.4 (2015-07-25) +++++++++++++++++++ +* Bug Fix: + - Fix issue #23? + - Fix issue #58 + - Fix issue #59 + +* Enhancement: + - Add filters support for ``Build Package.xml`` command, which is used to filter members which contains the input filters + - Add update feature for ``Build Package.xml`` command, which is used to add or remove members from exist package.xml + - Add keymap for some frequently-used commands + - Add visibility control for some CURD command on code file + - Aura related features + - Merge ``Deploy Lighting To Server`` command with ``Deploy File to Server`` command + - Merge ``Retrieve Lighting To Server`` command with ``Retrieve File to Server`` command + - Use file full name as key in ``component_metadata.sublime-settings``, originally, we use name as key, for example, originally, ``AccountController`` is key, now is ``AccountController.cls`` + - Change ``Diff With Server`` command to just visible when code file is ``classes, triggers, components or pages`` + +* New Feature: + - New ``Run Sync Test`` command for replacing ``Run Test`` feature + - Read code coverage information from local cache kept by ``Run Sync Test`` command + - New ``Retrieve from This Server`` command in the context menu + - New ``Diff With This Server`` command in the context menu + - New ``View File Attributes`` command in the context menu + +* Update: + - ``Quick Goto`` is switched to standard sublime build-in, I changed the mousemap to bind with the standard feature , with this feature, you can quickly view the symbols in sublime, for example, when you see a statement like this ``AccountUtil.populateField()``, you can put focus in the method name, hold down ``shift`` and triple-click your left mouse, sublime will open the ``AccountUtil`` class and put focus in the selected method + + Release 3.1.3 (2015-07-18) ++++++++++++++++++ * Fix issue #54 diff --git a/aura.py b/aura.py new file mode 100644 index 0000000..d730f71 --- /dev/null +++ b/aura.py @@ -0,0 +1,334 @@ +import sublime, sublime_plugin +import os +import re + +from . import context +from . import util +from . import processor + +class DeployLightingToServer(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(DeployLightingToServer, self).__init__(*args, **kwargs) + + def run(self, dirs, switch_project=True, source_org=None): + if switch_project: + return self.window.run_command("switch_project", { + "callback_options": { + "callback_command": "deploy_lighting_to_server", + "args": { + "switch_project": False, + "source_org": self.settings["default_project_name"], + "dirs": dirs + } + } + }) + + base64_package = util.build_aura_package(dirs) + processor.handle_deploy_thread(base64_package, source_org) + + def is_visible(self, dirs, switch_project=True): + if not dirs or len(dirs) == 0: return False + + self.settings = context.get_settings() + for _dir in dirs: + attributes = util.get_file_attributes(_dir) + metadata_folder = attributes["metadata_folder"] + if metadata_folder != "aura": return False + if self.settings["default_project_name"] not in _dir: + return False + + return True + +class PreviewLightingAppInServer(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(PreviewLightingAppInServer, self).__init__(*args, **kwargs) + + def run(self, app_name=None): + if app_name: + self.app_name = app_name + return self.preview_app() + + # Get all available apps to preview in the local aura path + aura_dir = os.path.join(self.settings["workspace"], "src", "aura") + self.app_names = [] + for dirpath, dirnames, filenames in os.walk(aura_dir): + for filename in filenames: + attributes = util.get_file_attributes(filename) + if attributes["extension"] == "app": + self.app_names.append(attributes["name"]) + self.app_names = sorted(self.app_names) + + # Check whether has available app to preview + if not self.app_names: + return Printer.get("error").write("No available app to preview") + + self.window.show_quick_panel(self.app_names, self.on_chosen) + + def on_chosen(self, index): + if index == -1: return + self.app_name = self.app_names[index] + + def preview_app(self): + instance = util.get_instance(self.settings) + if instance == "emea": instance = "eu0" + start_url = "https://%s.lightning.force.com/%s/%s.app" % ( + instance, self.namespace, self.app_name + ) + self.window.run_command("login_to_sfdc", {"startURL": start_url}) + + def is_enabled(self): + self.settings = context.get_settings() + metadata = util.get_described_metadata(self.settings["username"]) + self.namespace = metadata["organizationNamespace"] + if not self.namespace: + return False + + return True + +class PreviewThisAppInServer(sublime_plugin.TextCommand): + def run(self, edit): + self.view.window().run_command('preview_lighting_app_in_server', { + "app_name": self.app_name + }) + + def is_enabled(self): + if not self.view.file_name(): + return False + + attrs = util.get_file_attributes(self.view.file_name()) + if attrs["metadata_folder"] != 'aura' or attrs["extension"] != "app": + return False + self.app_name = attrs["name"] + + return True + + def is_visible(self): + return self.is_enabled() + +class RetrieveLightingFromServer(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(RetrieveLightingFromServer, self).__init__(*args, **kwargs) + + def run(self, dirs): + message = "Are you sure you really want to continue refreshing" + if sublime.ok_cancel_dialog(message, "Confirm?"): + processor.handle_retrieve_package( + self.types, + self.settings["workspace"], + ignore_package_xml=True + ) + + def is_visible(self, dirs): + if len(dirs) == 0: return False + self.settings = context.get_settings() + self.types = {} + for _dir in dirs: + if os.path.isfile(_dir): continue + base, _name = os.path.split(_dir) + base, _folder = os.path.split(base) + + # Check Metadata Type + if _folder != "aura": continue + + # Check Project Name + pn = self.settings["default_project_name"] + if pn not in _dir: continue + + if "AuraDefinitionBundle" in self.types: + self.types["AuraDefinitionBundle"].append(_name) + else: + self.types["AuraDefinitionBundle"] = [_name] + + # Check whether any aura components are chosen + if not self.types: return False + + return True + +class DestructLightingFromServer(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(DestructLightingFromServer, self).__init__(*args, **kwargs) + + def run(self, dirs): + if sublime.ok_cancel_dialog("Confirm to continue?"): + processor.handle_destructive_files(dirs, ignore_folder=False) + + def is_visible(self, dirs): + if len(dirs) == 0: return False + self.settings = context.get_settings() + for _dir in dirs: + attributes = util.get_file_attributes(_dir) + if attributes["metadata_folder"] != "aura": + return False + if not util.check_enabled(_dir, check_cache=False): + return False + + return True + +class CreateLightingElement(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingElement, self).__init__(*args, **kwargs) + + def run(self, dirs, element=""): + """ element: Component, Controller, Helper, Style, Documentation, Render + """ + + # Get template attribute + template_settings = sublime.load_settings("template.sublime-settings") + template = template_settings.get("template").get("AuraEelement").get(element) + extension = template["extension"] + body = template["body"] + + # JS Component is different with others + element_name = "%s%s%s" % ( + self.aura_name, + element if extension == ".js" else "", + extension + ) + + # Combine Aura element component name + element_file = os.path.join(self._dir, element_name) + + # If element file is already exist, just alert + if os.path.isfile(element_file): + return self.window.open_file(element_file) + + # Create Aura Element file + with open(element_file, "w") as fp: + fp.write(body) + + # If created succeed, just open it and refresh project + self.window.open_file(element_file) + self.window.run_command("refresh_folder_list") + + # Deploy Aura to server + self.window.run_command("deploy_lighting_to_server", { + "dirs": [self._dir], + "switch_project": False + }) + + def is_visible(self, dirs, element=""): + if not dirs or len(dirs) != 1: return False + self._dir = dirs[0] + + # Check whether project is the active one + settings = context.get_settings() + if settings["default_project_name"] not in self._dir: + return False + + # Check metadata folder + attributes = util.get_file_attributes(self._dir) + if attributes["metadata_folder"] != "aura": + return False + self.aura_name = attributes["name"] + + # Check lighting type + lighting_extensions = [] + for dirpath, dirnames, filenames in os.walk(self._dir): + for filename in filenames: + extension = filename[filename.find("."):] + lighting_extensions.append(extension) + + # Just Component and Application can have child elements + if ".cmp" in lighting_extensions or ".app" in lighting_extensions: + return True + + return False + +class CreateLightingDefinition(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingDefinition, self).__init__(*args, **kwargs) + + def run(self, _type=""): + self._type = _type + self.window.show_input_panel("Please Input Lighting Name: ", + "", self.on_input, None, None) + + def on_input(self, lighting_name): + # Create component to local according to user input + if not re.match('^[a-zA-Z]+\\w+$', lighting_name): + message = 'Invalid format, do you want to try again?' + if not sublime.ok_cancel_dialog(message): return + self.window.show_input_panel("Please Input Lighting Name: ", + "", self.on_input, None, None) + return + + # Get template attribute + template_settings = sublime.load_settings("template.sublime-settings") + template = template_settings.get("template").get("Aura").get(self._type) + + # Build dir for new lighting component + settings = context.get_settings() + component_dir = os.path.join(settings["workspace"], "src", "aura", lighting_name) + if not os.path.exists(component_dir): + os.makedirs(component_dir) + else: + message = "%s is already exist, do you want to try again?" % lighting_name + if not sublime.ok_cancel_dialog(message, "Try Again?"): return + self.window.show_input_panel("Please Input Lighting Name: ", + "", self.on_input, None, None) + return + + lihghting_file = os.path.join(component_dir, lighting_name+template["extension"]) + + # Create Aura lighting file + with open(lihghting_file, "w") as fp: + fp.write(template["body"]) + + # If created succeed, just open it and refresh project + window = sublime.active_window() + window.open_file(lihghting_file) + window.run_command("refresh_folder_list") + + # Deploy Aura to server + self.window.run_command("deploy_lighting_to_server", { + "dirs": [component_dir], + "switch_project": False + }) + +class CreateLightingApplication(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingApplication, self).__init__(*args, **kwargs) + + def run(self): + self.window.run_command("create_lighting_definition", { + "_type": "Application" + }) + + def is_enabled(self): + return util.check_action_enabled() + +class CreateLightingComponent(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingComponent, self).__init__(*args, **kwargs) + + def run(self): + self.window.run_command("create_lighting_definition", { + "_type": "Component" + }) + + def is_enabled(self): + return util.check_action_enabled() + +class CreateLightingInterface(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingInterface, self).__init__(*args, **kwargs) + + def run(self): + self.window.run_command("create_lighting_definition", { + "_type": "Interface" + }) + + def is_enabled(self): + return util.check_action_enabled() + +class CreateLightingEvent(sublime_plugin.WindowCommand): + def __init__(self, *args, **kwargs): + super(CreateLightingEvent, self).__init__(*args, **kwargs) + + def run(self): + self.window.run_command("create_lighting_definition", { + "_type": "Event" + }) + + def is_enabled(self): + return util.check_action_enabled() \ No newline at end of file diff --git a/config/commands/main.sublime-commands b/config/commands/main.sublime-commands index 497170e..d759a33 100644 --- a/config/commands/main.sublime-commands +++ b/config/commands/main.sublime-commands @@ -104,13 +104,13 @@ }, { - "caption": "HaoIDE: Deploy Lighting Element to Server", - "command": "deploy_lighting_element_to_server" + "caption": "HaoIDE: Preview Lighting App In Server", + "command": "preview_lighting_app_in_server" }, { - "caption": "HaoIDE: Preview Lighting App In Server", - "command": "preview_lighting_app_in_server" + "caption": "HaoIDE: Preview This Lighting App In Server", + "command": "preview_this_app_in_server" }, { diff --git a/config/keymap/Default (Linux).sublime-keymap b/config/keymap/Default (Linux).sublime-keymap index 8986294..4e775a9 100644 --- a/config/keymap/Default (Linux).sublime-keymap +++ b/config/keymap/Default (Linux).sublime-keymap @@ -19,7 +19,19 @@ } }, - // Deploy to this server + // Diff file with this server, which means not switch project + {"keys": ["alt+shift+d"], "command": "diff_with_server", + "args": { + "switch": false + } + }, + // Retrieve file from this server, which means not switch project + {"keys": ["alt+shift+r"], "command": "retrieve_file_from_server", + "args": { + "switch": false + } + }, + // Deploy file to this server, which means not switch project {"keys": ["alt+shift+s"], "command": "deploy_file_to_this_server"}, // Update the default project @@ -55,6 +67,13 @@ // Save current component to server {"keys": ["ctrl+alt+s"], "command": "save_to_server"}, + // Compile file + {"keys": ["alt+shift+c"], "command": "save_to_server", + "args": { + "is_check_only": true + } + }, + // Choose a test class in the list and run test class {"keys": ["alt+r","alt+t"],"command": "run_one_test"}, @@ -68,6 +87,9 @@ // Problem here, don't know why? {"keys": ["ctrl+alt+t"], "command": "run_test"}, + // Run sync test + {"keys": ["alt+shift+u"], "command": "run_sync_test"}, + // Refresh current component in edit mode {"keys": ["ctrl+alt+r"], "command": "refresh_file_from_server"}, diff --git a/config/keymap/Default (OSX).sublime-keymap b/config/keymap/Default (OSX).sublime-keymap index ddc0c9f..183d083 100644 --- a/config/keymap/Default (OSX).sublime-keymap +++ b/config/keymap/Default (OSX).sublime-keymap @@ -19,7 +19,19 @@ } }, - // Deploy to this server + // Diff file with this server, which means not switch project + {"keys": ["alt+shift+d"], "command": "diff_with_server", + "args": { + "switch": false + } + }, + // Retrieve file from this server, which means not switch project + {"keys": ["command+shift+r"], "command": "retrieve_file_from_server", + "args": { + "switch": false + } + }, + // Deploy file to this server, which means not switch project {"keys": ["shift+command+s"], "command": "deploy_file_to_this_server"}, // Update the default project @@ -49,6 +61,13 @@ // Save current component to server {"keys": ["super+alt+s"], "command": "save_to_server"}, + // Compile file + {"keys": ["command+shift+c"], "command": "save_to_server", + "args": { + "is_check_only": true + } + }, + // Choose a test class in the list and run test class {"keys": ["alt+t"],"command": "run_one_test"}, @@ -62,6 +81,9 @@ // Problem here, don't know why? {"keys": ["super+alt+t"], "command": "run_test"}, + // Run sync test + {"keys": ["command+shift+u"], "command": "run_sync_test"}, + // Refresh current component in edit mode {"keys": ["super+alt+r"], "command": "refresh_file_from_server"}, diff --git a/config/keymap/Default (Windows).sublime-keymap b/config/keymap/Default (Windows).sublime-keymap index dd00253..7feeb8e 100644 --- a/config/keymap/Default (Windows).sublime-keymap +++ b/config/keymap/Default (Windows).sublime-keymap @@ -19,7 +19,19 @@ } }, - // Deploy to this server + // Diff file with this server, which means not switch project + {"keys": ["alt+shift+d"], "command": "diff_with_server", + "args": { + "switch": false + } + }, + // Retrieve file from this server, which means not switch project + {"keys": ["alt+shift+r"], "command": "retrieve_file_from_server", + "args": { + "switch": false + } + }, + // Deploy file to this server, which means not switch project {"keys": ["alt+shift+s"], "command": "deploy_file_to_this_server"}, // Update the default project @@ -52,6 +64,13 @@ // Save current component to server {"keys": ["ctrl+alt+s"], "command": "save_to_server"}, + // Compile file + {"keys": ["alt+shift+c"], "command": "save_to_server", + "args": { + "is_check_only": true + } + }, + // Choose a test class in the list and run test class {"keys": ["alt+t"],"command": "run_one_test"}, @@ -65,6 +84,9 @@ // Problem here, don't know why? {"keys": ["ctrl+alt+t"], "command": "run_test"}, + // Run sync test + {"keys": ["alt+shift+u"], "command": "run_sync_test"}, + // Refresh current component in edit mode {"keys": ["ctrl+alt+r"], "command": "refresh_file_from_server"}, diff --git a/config/menus/Context.sublime-menu b/config/menus/Context.sublime-menu index cad3ee3..322e617 100644 --- a/config/menus/Context.sublime-menu +++ b/config/menus/Context.sublime-menu @@ -11,19 +11,27 @@ // {"caption": "Rename Metadata", "command": "rename_metadata"}, {"caption": "Retrieve from Server", "command": "retrieve_file_from_server"}, - // {"caption": "Rename from Server", "command": "rename_metadata"}, - {"caption": "Deploy to Server", "command": "deploy_file_to_server"}, - {"caption": "Deploy to This Server", "command": "deploy_file_to_this_server"}, - - {"caption": "Deploy Lighting Element to Server", "command": "deploy_lighting_element_to_server"}, - {"caption": "Destruct from Server", "command": "destruct_file_from_server"}, + {"caption": "Diff With Server", "command": "diff_with_server"}, + {"caption": "-"}, - {"caption": "Diff With Server", "command": "diff_with_server"}, + // Lighting App preview command + {"caption": "Preview This App In Server", "command": "preview_this_app_in_server"}, + {"caption": "Diff With This Server", "command": "diff_with_server", + "args": { + "switch": false + } + }, + {"caption": "Retrieve from This Server", "command": "retrieve_file_from_server", + "args": { + "switch": false + } + }, + {"caption": "Deploy to This Server", "command": "deploy_file_to_this_server"}, {"caption": "-"}, @@ -84,9 +92,11 @@ {"caption": "Execute Anonymous","command": "execute_anonymous"}, {"caption": "Execute Query","command": "execute_query"}, {"caption": "Run Test Class","command": "run_test"}, + {"caption": "Run Sync Test","command": "run_sync_test"}, {"caption": "Preview Page in Server","command": "preview_page"}, {"caption": "-"}, + {"caption": "View File Attributes","command": "view_file_attributes"}, {"caption": "View Code Coverage","command": "view_code_coverage"}, {"caption": "View Debug Log Detail","command": "view_debug_log_detail"}, {"caption": "View Id in Salesforce Web","command": "view_id_in_sfdc_web"}, diff --git a/config/messages/3.0.0.md b/config/messages/3.0.0.md deleted file mode 100644 index 94997c1..0000000 --- a/config/messages/3.0.0.md +++ /dev/null @@ -1,15 +0,0 @@ -Build 3.0.0 ------------ -Release Date: 26 May 2015 - -* Bug fix: - - Fix bug #38 - - Fix bug for SOQL fields completion - - Fix bug for attributes completion for value of ``apex:includeScript`` - -* New - - Add a new snippet named ``Page - field label.sublime-snippet`` - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded ------------ \ No newline at end of file diff --git a/config/messages/3.0.1.md b/config/messages/3.0.1.md deleted file mode 100644 index a8fd601..0000000 --- a/config/messages/3.0.1.md +++ /dev/null @@ -1,27 +0,0 @@ -Build 3.0.1 ------------ -Release Date: 4 June 2015 - -* Bug fix: - - Fix bug #39 - - Fix bug #40 - - Fix bug for SOQL completion - -* Enhancement: - - Enhancement for boolean attribute completion of standard visualforce component - - Set ``word_wrap`` setting of new view to false when describe sObject - - Keep attributes of all metadataObjects to local ``component_metadata.sublime-settings`` - - Prevent potential issue caused by change of ``component_metadata.sublime-settings`` - -* Update: - - Add output panel message for ``describe_metadata`` command - - Disable document reference reload feature - - Add a ``salesforce_reference.sublime-settings`` for ``Document > Open Document`` in the main menu - -* New API for metadata: - - Add a new ``read_metadata`` method for ``metadata.py``, which will be used for ``diff_with_server`` feature in the future - - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded ------------ \ No newline at end of file diff --git a/config/messages/3.0.2.md b/config/messages/3.0.2.md deleted file mode 100644 index 9d8d944..0000000 --- a/config/messages/3.0.2.md +++ /dev/null @@ -1,26 +0,0 @@ -Build 3.0.2 ------------ -Release Date: 7 June 2015 - -* Bug fix: - - Fix NoneType exception in the console when open context menu, this is caused by release 3.0.1 - - Fix bug for ``Debug > Track All`` in the main menu - -* Enhancement - - Duplicate save_to_server check logic change: use file name with extension but not only file name, as the original design, if the controller name is same with page name, if you are saving page, you can't save the related controller at the same time - - Add timeout for query of conflict checking when ``save_to_server`` - - Prevent duplicate save conflict check when ``save_to_server``, as the original design, if you latest saving is interrupted, when you save it again, plugin will delete the metadata container Id for the saving file, at this time, save conflict checking will be executed again. - -* New: - - Add sObject completion for ``tooling sObjects``, for example, ``Validation Rule``, ``WorkflowRule``, ``ValidationRule``, ``WorkflowFieldUpdate``, ``WorkflowOutboundMessage``, ``WorkflowAlert`` or ``WorkflowTask`` - - Add * support for ``export query to CSV`` or ``export tooling query to CSV``, if you use * in the query statement, plugin will get all fields of this object and set them as the column headers - - Add export command for tooling query into the ``Data Loader`` in the main menu, you can use this command to export records for tooling objects - - Add a new markdown document related to debug - - Add a menu item for quick accessing document related to debug - -* Update: - - Update the menu item names and location in command palette and the ``Debug`` of main menu - - Change the default key binding for ``Debug > Run Test`` in the main menu - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.3.md b/config/messages/3.0.3.md deleted file mode 100644 index e8a5afb..0000000 --- a/config/messages/3.0.3.md +++ /dev/null @@ -1,17 +0,0 @@ -Build 3.0.3 ------------ -Release Date: 11 June 2015 - -* Bug Fix: - - Fix duplicate save check bug caused by release 3.0.2 - - Fix fields completion bug for cross sObjects between tooling and non-tooling, for example ``User``, ``RecordType`` - -* Enhancement: - - Add session expired message for ``describe_metadata`` - - Enhancement for ``refresh_file_from_server`` - -* Update - - Update pop-up compile message for ``save_to_server`` command - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.4.md b/config/messages/3.0.4.md deleted file mode 100644 index 4d3d1c9..0000000 --- a/config/messages/3.0.4.md +++ /dev/null @@ -1,24 +0,0 @@ -Build 3.0.4 ------------ -Release Date: 15 June 2015 - -* Bug Fix: - - Fix bug for issue #41 - - Fix bug for ``delete_file_from_server`` keybinding for windows - - Fix bug for ``auto_update_on_save`` feature in windows - - Fix ``KeyError: '\n\n'`` for converting complex JSON to Apex - -* Enhancement: - - Improve the regular expression for Apex class method completion - - Improve the regular expression for visualforce component attribute completion - - Improve the visualforce tag name completion, add ``>`` for tag name automatically - - As the original design, you need to input your JSON when you execute JSON related commands, since this version, you just need to open any JSON file or select valid JSON content - - Add ``JSON/XML Tool`` into context menu, which is same with ``Utilities`` in main menu - - Update content for some docs - -* New Feature: - - Add attribute completion for custom component - - Add document for all code completion, you can see the link in the plugin home page - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.5.md b/config/messages/3.0.5.md deleted file mode 100644 index 26be437..0000000 --- a/config/messages/3.0.5.md +++ /dev/null @@ -1,12 +0,0 @@ -Build 3.0.5 ------------ -Release Date: 16 June 2015 - -* Bug Fix: - - Custom component attributes completion bug when component file is not exist in the target path - -* Enhancement: - - Improve regular expression for SOQL fields completion - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.6.md b/config/messages/3.0.6.md deleted file mode 100644 index fcdb8f0..0000000 --- a/config/messages/3.0.6.md +++ /dev/null @@ -1,11 +0,0 @@ -Build 3.0.6 ------------ -Release Date: 23 June 2015 - -* Bug Fix: - - Merge pull request #42 by @pgAdmin(https://github.com/pgAdmin) - - Merge pull request #43 by @reyesml(https://github.com/reyesml), fixed issue #6 - - Fix bug for ``export_workbook`` feature - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.7.md b/config/messages/3.0.7.md deleted file mode 100644 index 30e70b1..0000000 --- a/config/messages/3.0.7.md +++ /dev/null @@ -1,18 +0,0 @@ -Build 3.0.7 ------------ -Release Date: 26 June 2015 - -* Bug Fix: - - Fix issue #46 - - Fix bugs caused by ``describe_global`` change in the ``tooling.py`` - -* Enhancement - - Merge pull request #45 by @reyesml(https://github.com/reyesml) - -* New - - Add a snippets: ``Page Variable - get and set in one line.sublime-snippet`` - - Add a snippets: ``Page Variable - get and set in multiply line.sublime-snippet`` - - Add a new command for building package.xml for whole org - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.8.md b/config/messages/3.0.8.md deleted file mode 100644 index 2c3ac09..0000000 --- a/config/messages/3.0.8.md +++ /dev/null @@ -1,19 +0,0 @@ -Build 3.0.8 ------------ -Release Date: 28 June 2015 - -* Bug Fix: - - Fix bug when build package.xml for whole org - -* Enhancement: - - Display chosen sObject Name when input trigger name - - Enhancement for #39, open a new view, set status bar and close the new view - - Add success message for ``extract_to_here`` command - - Update all snippets - -* New: - - Add a quick link to view all snippets, see it in plugin home page - - Add command to access all snippets in ``Utilities`` of main menu - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.0.9.md b/config/messages/3.0.9.md deleted file mode 100644 index 5f8baa7..0000000 --- a/config/messages/3.0.9.md +++ /dev/null @@ -1,14 +0,0 @@ -Build 3.0.9 ------------ -Release Date: 02 July 2015 - -* Bug Fix: - - Fix bug for snippet ``SOQL - SELECT * FROM.sublime-snippet`` - - Fix bug for ``extract_to_here`` command - -* Enhancement: - - Don't need confirmation to reload project cache after choose metadata objects - - In order to avoid timeout exception, increase max_retries from 5 to 10 for retrieve zipFile request - -* Note: - - You should restart your sublime after ``HaoIDE`` is upgraded \ No newline at end of file diff --git a/config/messages/3.1.4.md b/config/messages/3.1.4.md new file mode 100644 index 0000000..a891c61 --- /dev/null +++ b/config/messages/3.1.4.md @@ -0,0 +1,31 @@ +Build 3.1.4 +----------- +Release Date: 25 July 2015 + +* Bug Fix: + - Fix issue #23? + - Fix issue #58 + - Fix issue #59 + +* Enhancement: + - Add filters support for ``Build Package.xml`` command, which is used to filter members which contains the input filters + - Add update feature for ``Build Package.xml`` command, which is used to add or remove members from exist package.xml + - Add keymap for some frequently-used commands + - Add visibility control for some CURD command on code file + - Aura related features + - Merge ``Deploy Lighting To Server`` command with ``Deploy File to Server`` command + - Merge ``Retrieve Lighting To Server`` command with ``Retrieve File to Server`` command + - Use file full name as key in ``component_metadata.sublime-settings``, originally, we use name as key, for example, originally, ``AccountController`` is key, now is ``AccountController.cls`` + - Change ``Diff With Server`` command to just visible when code file is ``classes, triggers, components or pages`` + +* New Feature: + - New ``Run Sync Test`` command for replacing ``Run Test`` feature + - Read code coverage information from local cache kept by ``Run Sync Test`` command + - New ``Retrieve from This Server`` command in the context menu + - New ``Diff With This Server`` command in the context menu + - New ``View File Attributes`` command in the context menu + +* Update: + - ``Quick Goto`` is switched to standard sublime build-in, I changed the mousemap to bind with the standard feature , with this feature, you can quickly view the symbols in sublime, for example, when you see a statement like this ``AccountUtil.populateField()``, you can put focus in the method name, hold down ``shift`` and triple-click your left mouse, sublime will open the ``AccountUtil`` class and put focus in the selected method + +* Restart your sublime when new version is installed \ No newline at end of file diff --git a/config/mousemap/Default (Linux).sublime-mousemap b/config/mousemap/Default (Linux).sublime-mousemap index 84663b5..34482f6 100644 --- a/config/mousemap/Default (Linux).sublime-mousemap +++ b/config/mousemap/Default (Linux).sublime-mousemap @@ -1,31 +1,31 @@ [ - // Press shift and Click Left Mouse for twice will open class file in the background + // Button 1 is the left mouse + // Press shift and Click Button 1 for twice will open class file in the foreground { - "button": "button1", "count": 3, "modifiers": ["shift"], - "press_command": "goto_component", - "press_args": {"is_background": true} + "button": "button1", "count": 2, "modifiers": ["shift"], + "press_command": "context_goto_definition" }, - // Press shift and Click Left Mouse for twice will open class file in the foreground + // Press shift and Click Button 1 for triple will open class file in the background { - "button": "button1", "count": 2, "modifiers": ["shift"], + "button": "button1", "count": 3, "modifiers": ["shift"], "press_command": "goto_component", "press_args": {"is_background": false} }, - // Press shift and Click Left Mouse will retrieve debug log detail by id + // Press shift and Click Button 1 will retrieve debug log detail by id { "button": "button1", "count": 1, "modifiers": ["alt"], "press_command": "view_debug_log_detail" }, - // Press alt and Click Left Mouse for twice will quick view the code coverage + // Press alt and Click Button 1 for twice will quick view the code coverage { "button": "button1", "count": 3, "modifiers": ["alt"], "press_command": "view_selected_code_coverage" }, - // Press alt and Click Left Mouse for triple will cancel deployment of specified task Id + // Press alt and Click Button 1 for triple will cancel deployment of specified task Id { "button": "button1", "count": 2, "modifiers": ["alt"], "press_command": "cancel_deployment" diff --git a/config/mousemap/Default (OSX).sublime-mousemap b/config/mousemap/Default (OSX).sublime-mousemap index 84663b5..0503bbd 100644 --- a/config/mousemap/Default (OSX).sublime-mousemap +++ b/config/mousemap/Default (OSX).sublime-mousemap @@ -1,14 +1,14 @@ [ - // Press shift and Click Left Mouse for twice will open class file in the background + // Button 1 is the left mouse + // Press shift and Click Left Mouse for twice will open class file in the foreground { - "button": "button1", "count": 3, "modifiers": ["shift"], - "press_command": "goto_component", - "press_args": {"is_background": true} + "button": "button1", "count": 2, "modifiers": ["shift"], + "press_command": "context_goto_definition" }, - // Press shift and Click Left Mouse for twice will open class file in the foreground + // Press shift and Click Left Mouse for triple will open class file in the background { - "button": "button1", "count": 2, "modifiers": ["shift"], + "button": "button1", "count": 3, "modifiers": ["shift"], "press_command": "goto_component", "press_args": {"is_background": false} }, diff --git a/config/mousemap/Default (Windows).sublime-mousemap b/config/mousemap/Default (Windows).sublime-mousemap index 84663b5..289404b 100644 --- a/config/mousemap/Default (Windows).sublime-mousemap +++ b/config/mousemap/Default (Windows).sublime-mousemap @@ -1,31 +1,31 @@ -[ - // Press shift and Click Left Mouse for twice will open class file in the background +[ + // Button 1 is the left mouse + // Press shift and Click Button 1 for twice will open class file in the foreground { - "button": "button1", "count": 3, "modifiers": ["shift"], - "press_command": "goto_component", - "press_args": {"is_background": true} + "button": "button1", "count": 2, "modifiers": ["shift"], + "press_command": "context_goto_definition" }, - // Press shift and Click Left Mouse for twice will open class file in the foreground + // Press shift and Click Button 1 for triple will open class file in the background { - "button": "button1", "count": 2, "modifiers": ["shift"], + "button": "button1", "count": 3, "modifiers": ["shift"], "press_command": "goto_component", "press_args": {"is_background": false} }, - // Press shift and Click Left Mouse will retrieve debug log detail by id + // Press shift and Click Button 1 will retrieve debug log detail by id { "button": "button1", "count": 1, "modifiers": ["alt"], "press_command": "view_debug_log_detail" }, - // Press alt and Click Left Mouse for twice will quick view the code coverage + // Press alt and Click Button 1 for twice will quick view the code coverage { "button": "button1", "count": 3, "modifiers": ["alt"], "press_command": "view_selected_code_coverage" }, - // Press alt and Click Left Mouse for triple will cancel deployment of specified task Id + // Press alt and Click Button 1 for triple will cancel deployment of specified task Id { "button": "button1", "count": 2, "modifiers": ["alt"], "press_command": "cancel_deployment" diff --git a/config/settings/package.sublime-settings b/config/settings/package.sublime-settings index 48d059a..8f7ccc4 100644 --- a/config/settings/package.sublime-settings +++ b/config/settings/package.sublime-settings @@ -1,6 +1,6 @@ { "name": "HaoIDE", - "version": "3.1.3", + "version": "3.1.4", "description": "HaoIDE is a Sublime Text 3 plugin for Salesforce and used for swift development on Force.com", "author": "Hao Liu", "email": "mouse.mliu@gmail.com", diff --git a/config/settings/template.sublime-settings b/config/settings/template.sublime-settings index 5d4e4ca..8ce3148 100644 --- a/config/settings/template.sublime-settings +++ b/config/settings/template.sublime-settings @@ -92,7 +92,7 @@ "Test Class": { "extension": ".cls", "description": "Exception Class", - "body": "@isTest\nprivate class class_name {\n\t\n}" + "body": "@isTest\nprivate class class_name {\n \n}" } }, diff --git a/config/snippets/Apex/Method - test method.sublime-snippet b/config/snippets/Apex/Method - test method.sublime-snippet index e0270fb..583bd94 100644 --- a/config/snippets/Apex/Method - test method.sublime-snippet +++ b/config/snippets/Apex/Method - test method.sublime-snippet @@ -1,7 +1,7 @@ tm diff --git a/docs/debug.md b/docs/debug.md index 5a0b94b..797de72 100644 --- a/docs/debug.md +++ b/docs/debug.md @@ -37,7 +37,7 @@ There has a ```log_levels``` setting in the default settings to control the anon Default Value Description - + trace_flag @@ -69,10 +69,13 @@ There has a ```log_levels``` setting in the default settings to control the anon - Put the focus in the LogId got by fetch command, press alt and click left button, the debug log detail will be retrieved and displayed in the new view. ## Run Test -* By Main Menu: click ``HaoIDE`` > ``Debug`` > ``Run Test``, choose the test class and press enter, check the progress in the status bar until succeed message appeared, and then a new view with the test result will be open. -* By Context Menu: in the context of opened class, click ``HaoIDE`` > ``Run Test Class``, check the progress in the status bar until succeed message appeared, and then a new view with the test result will be open. +* This feature will be deprecated in the release v3.1.5, because this feature is executed in async way, it's very slow. + +## Run Sync Test +* Click ``HaoIDE > Run Sync Test`` in the context menu or press alt/command + shift + u, you can get the test run result and related coverage report +* The coverage information will be kept in ``.config/coverages.json`` cache file, when you execute ``view_code_coverage`` command next time, plugin will read code coverage information from here ## View Code Coverage * This feature just works when api version is >= 29.0 * In the context menu of open class or trigger, click ``HaoIDE`` > ``View Code Coverage`` in the context menu, wait for the end of the progress on the status bar, you will see the code coverage percentage in the console and a new view with not covered highlight lines. -* Put the focus in the ApexClass Name, press ``alt`` and click left button for twice, the code coverage of specified class will be retrieved and displayed in the new view. \ No newline at end of file +* Put the focus in the ApexClass Name, press ``alt`` and click left button for twice, the code coverage of specified class will be retrieved and displayed in the new view. diff --git a/events.py b/events.py index 379ab75..07b188b 100644 --- a/events.py +++ b/events.py @@ -23,6 +23,15 @@ def on_load_async(self, view): util.display_active_project(view) + # Add types settings for build_package_xml command + if view.file_name(): + cname = os.path.basename(view.file_name()) + if "package.xml" in cname.lower(): + with open(view.file_name(), "rb") as fp: + content = fp.read() + types = util.build_package_types(content) + view.settings().set("types", types) + def on_post_save_async(self, view): settings = context.get_settings(); if not view.file_name(): return diff --git a/main.py b/main.py index c685ebb..981e730 100644 --- a/main.py +++ b/main.py @@ -167,19 +167,20 @@ def run(self, edit): class DiffWithServer(sublime_plugin.TextCommand): def run(self, edit, switch=True, source_org=None): - settings = context.get_settings() - if switch: return self.view.window().run_command("switch_project", { "callback_options": { "callback_command": "diff_with_server", "args": { "switch": False, - "source_org": settings["default_project_name"] + "source_org": self.settings["default_project_name"] } } }) + if not source_org: + source_org = self.settings["default_project_name"] + file_name = self.view.file_name() attr = util.get_component_attribute(file_name)[0] @@ -196,9 +197,19 @@ def run(self, edit, switch=True, source_org=None): def is_enabled(self): self.file_name = self.view.file_name() - if not self.file_name: return False + if not self.file_name: + return False + + self.settings = context.get_settings() + attributes = util.get_file_attributes(self.file_name) + if attributes["metadata_folder"] not in ["classes", "triggers", "pages", "components"]: + return False + return True + def is_visible(self): + return self.is_enabled() + class ShowMyPanel(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(ShowMyPanel, self).__init__(*args, **kwargs) @@ -515,38 +526,59 @@ def run(self, edit, mark): class ViewCodeCoverageCommand(sublime_plugin.TextCommand): def run(self, edit): - # Get file_name and component_attribute - file_name = self.view.file_name() - component_attribute, component_name = util.get_component_attribute(file_name) - - # Handle Save Current Component - processor.handle_view_code_coverage(component_name, component_attribute, self.body) + util.view_coverage(self.attributes["name"], self.file_name, self.body) def is_enabled(self): # Must Be File - if not self.view.file_name(): return False + if not self.view.file_name(): + return False + self.file_name = self.view.file_name() # Must be valid component - is_enabled = util.check_enabled(self.view.file_name()) - if not is_enabled: return False + if not util.check_enabled(self.file_name): + return False # Must be class or trigger - attributes = util.get_file_attributes(self.view.file_name()) - if not attributes["extension"]: return False - if attributes["extension"] not in ["cls", "trigger"]: return False + self.attributes = util.get_file_attributes(self.file_name) + if not self.attributes["extension"]: + return False + if self.attributes["metadata_folder"] not in ["classes", "triggers"]: + return False # Can't be Test Class - self.body = open(self.view.file_name(), encoding="utf-8").read() - if "@istest" in self.body.lower(): return False + self.body = open(self.file_name, encoding="utf-8").read() + if "@istest" in self.body.lower(): + return False return True class ViewSelectedCodeCoverageCommand(sublime_plugin.TextCommand): def run(self, edit): - self.view.run_command("goto_component", {"is_background": False}) + # Keep all open views + openViewIds = [v.id() for v in sublime.active_window().views()] + + # Open the related code file + self.view.run_command("goto_component", { + "is_background": False + }) + + # Get the view of open code file view = sublime.active_window().active_view() + + # Execute view_code_coverage command view.run_command("view_code_coverage") - view.run_command("close") + + # Get the coverage view + coverageView = sublime.active_window().active_view() + + # Focus on the view of open code file + # Close the view of open code file + if view.id() not in openViewIds: + sublime.active_window().focus_view(view) + sublime.active_window().run_command("close") + + # Move focus to the coverage view + sublime.active_window().focus_view(coverageView) class NewViewCommand(sublime_plugin.TextCommand): """ @@ -594,16 +626,14 @@ def run(self, edit, view_id=None, view_name="", input="", point=0, erase_all=Fal if erase_all: view.erase(edit, sublime.Region(0, view.size())) view.insert(edit, point, input) -class RefreshFolderCommand(sublime_plugin.WindowCommand): +class RefreshFolder(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): - super(RefreshFolderCommand, self).__init__(*args, **kwargs) + super(RefreshFolder, self).__init__(*args, **kwargs) def run(self, dirs): message = "Are you sure you really want to refresh these folders" - if not sublime.ok_cancel_dialog(message, "Refresh Folders"): return - - # Retrieve file from server - processor.handle_refresh_folder(self.types) + if sublime.ok_cancel_dialog(message, "Refresh Folders"): + processor.handle_refresh_folder(self.types) def is_visible(self, dirs): if not dirs: return False @@ -660,22 +690,27 @@ def is_enabled(self): return True class RetrieveFileFromServer(sublime_plugin.TextCommand): - def run(self, edit): + def run(self, edit, switch=True): files = [self.view.file_name()] sublime.active_window().run_command("retrieve_files_from_server", { - "files": files + "files": files, + "switch": switch }) def is_enabled(self): if not self.view or not self.view.file_name(): return False self.settings = context.get_settings() - metadata_folder = util.get_metadata_folder(self.view.file_name()) + attributes = util.get_file_attributes(self.view.file_name()) + metadata_folder = attributes["metadata_folder"] if metadata_folder not in self.settings["all_metadata_folders"]: return False if not util.check_enabled(self.view.file_name(), check_cache=False): return False return True + def is_visible(self): + return self.is_enabled() + class RetrieveFilesFromServer(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(RetrieveFilesFromServer, self).__init__(*args, **kwargs) @@ -708,6 +743,10 @@ def run(self, files, switch=True, source_org=None): if metadata_object_attr["inFolder"] == "true": name = "%s/%s" % (attributes["folder"], attributes["name"]) + # If file is AuraDefinitionBundle, we need to add folder + if metadata_folder == "aura": + name = "%s" % attributes["folder"] + if metadata_object in types: types[metadata_object].append(name) else: @@ -752,6 +791,9 @@ def is_enabled(self): return True + def is_visible(self): + return self.is_enabled() + class DestructFilesFromServer(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(DestructFilesFromServer, self).__init__(*args, **kwargs) @@ -822,7 +864,8 @@ def is_enabled(self): self.files.append(_file) # If there is no sfdc code file, just disable this command - if not self.files: return False + if not self.files: + return False return True @@ -871,12 +914,15 @@ def run(self, edit, switch=True): def is_enabled(self): if not self.view or not self.view.file_name(): return False self.settings = context.get_settings() - _folder = util.get_metadata_folder(self.view.file_name()) - if _folder not in self.settings["all_metadata_folders"]: + attributes = util.get_file_attributes(self.view.file_name()) + if attributes["metadata_folder"] not in self.settings["all_metadata_folders"]: return False return True + def is_visible(self): + return self.is_enabled() + class DeployFileToThisServer(sublime_plugin.TextCommand): def run(self, edit): files = [self.view.file_name()] @@ -887,6 +933,9 @@ def run(self, edit): def is_enabled(self): return util.check_enabled(self.view.file_name(), check_cache=False) + def is_visible(self): + return self.is_enabled() + class DeployFilesToServer(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(DeployFilesToServer, self).__init__(*args, **kwargs) @@ -927,8 +976,8 @@ def is_visible(self, files): self.settings = context.get_settings() for _file in files: if not os.path.isfile(_file): continue # Ignore folder - _folder = util.get_metadata_folder(_file) - if _folder not in self.settings["all_metadata_folders"]: + attributes = util.get_file_attributes(_file) + if attributes["metadata_folder"] not in self.settings["all_metadata_folders"]: return False return True @@ -1144,31 +1193,36 @@ def __init__(self, *args, **kwargs): def run(self): pass -class RunSyncTestClassesCommand(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(RunSyncTestClassesCommand, self).__init__(*args, **kwargs) +class RunSyncTest(sublime_plugin.TextCommand): + def run(self, edit): + processor.handle_run_sync_test([self.cname]) - def run(self, files): - processor.handle_run_sync_test_classes(self.class_names) + def is_enabled(self): + if not self.view or not self.view.file_name(): return False + self.file_name = self.view.file_name() + self.settings = context.get_settings() + self.component_attribute, self.cname = util.get_component_attribute(self.file_name) + if not self.component_attribute: + return False - def is_enabled(self, files): - # Check whether any classes are chosen - if len(files) == 0: return False + if "is_test" not in self.component_attribute or \ + not self.component_attribute["is_test"]: + return False - # Check whether there are test class in chosen classes - self.class_names = [] - for f in files: - component_attribute, name = util.get_component_attribute(f) - if not component_attribute or not component_attribute["is_test"]: - continue + if "namespacePrefix" in self.component_attribute: + self.cname = "%s.%s" % ( + self.component_attribute["namespacePrefix"], + self.cname + ) + + return True - self.class_names.append(name) - - return len(self.class_names) > 0 + def is_visible(self): + return self.is_enabled() -class RunAsyncTestClassesCommand(sublime_plugin.WindowCommand): +class RunAsyncTest(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): - super(RunAsyncTestClassesCommand, self).__init__(*args, **kwargs) + super(RunAsyncTest, self).__init__(*args, **kwargs) def run(self, files): processor.handle_run_async_test_classes(self.class_ids) @@ -1188,6 +1242,9 @@ def is_enabled(self, files): return len(self.class_ids) > 0 + def is_visible(self): + return self.is_enabled() + class RunTestCommand(sublime_plugin.TextCommand): def run(self, view): # Get component_attribute by file_name @@ -1213,6 +1270,9 @@ def is_enabled(self): return True + def is_visible(self): + return self.is_enabled() + class TrackAllDebugLogs(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(TrackAllDebugLogs, self).__init__(*args, **kwargs) @@ -1444,6 +1504,9 @@ def is_enabled(self): return True + def is_visible(self): + return self.is_enabled() + class CreateApexTriggerCommand(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(CreateApexTriggerCommand, self).__init__(*args, **kwargs) @@ -1594,7 +1657,8 @@ def create_component(self): # Build Post body data = { - "name": self.component_name, self.markup_or_body: body + "name": self.component_name, + self.markup_or_body: body } if self.component_type == "ApexClass": @@ -1609,324 +1673,6 @@ def create_component(self): self.markup_or_body, file_name) -class DeployLightingElementToServer(sublime_plugin.TextCommand): - def run(self, edit): - if self.view.is_dirty(): self.view.run_command("save") - processor.handle_deploy_thread(util.build_aura_package([self.lightning_dir])) - - def is_visible(self): - if not self.view or not self.view.file_name(): return False - self.settings = context.get_settings() - _file = self.view.file_name() - self.lightning_dir, lighting_component_name = os.path.split(_file) - aura_path, lighting_name = os.path.split(self.lightning_dir) - base, meta_type = os.path.split(aura_path) - - if meta_type != "aura": return False - if self.settings["default_project_name"] not in _file: - return False - - return True - -class DeployLightingToServer(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(DeployLightingToServer, self).__init__(*args, **kwargs) - - def run(self, dirs, switch_project=True): - # Get the package path to deploy - self.dirs = dirs - - if not switch_project: - base64_package = util.build_aura_package(dirs) - processor.handle_deploy_thread(base64_package) - return - - # Get the settings - self.settings = context.get_settings() - - # Keep the source_org - self.source_org = self.settings["default_project_name"] - - # Choose the target ORG to deploy - self.projects = self.settings["projects"] - self.projects = ["(" + ('Active' if self.projects[p]["default"] else - 'Inactive') + ") " + p for p in self.projects] - self.projects = sorted(self.projects, reverse=False) - self.window.show_quick_panel(self.projects, self.on_done) - - def on_done(self, index): - if index == -1: return - # Change the chosen project as default - # Split with ") " and get the second project name - default_project = self.projects[index].split(") ")[1] - util.switch_project(default_project) - - base64_package = util.build_aura_package(self.dirs) - processor.handle_deploy_thread(base64_package, self.source_org) - - def is_visible(self, dirs, switch_project=True): - if not dirs or len(dirs) == 0: return False - - self.settings = context.get_settings() - for _dir in dirs: - base, aura_name = os.path.split(_dir) - base, meta_type = os.path.split(base) - if meta_type != "aura": return False - if self.settings["default_project_name"] not in _dir: - return False - - return True - -class PreviewLightingAppInServer(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(PreviewLightingAppInServer, self).__init__(*args, **kwargs) - - def run(self): - self.aura_attrs = util.populate_lighting_applications() - self.app_names = list(self.aura_attrs.keys()) - self.window.show_quick_panel(self.app_names, self.on_chosen) - - def on_chosen(self, index): - if index == -1: return - app_name = self.app_names[index] - app_attr = self.aura_attrs[app_name] - - settings = context.get_settings() - session = util.get_session_info(settings) - instance_url = session["instance_url"] - instance = instance_url[8:instance_url.index(".")] - if instance == "emea": instance = "eu0" - start_url = "https://%s.lightning.force.com/%s/%s.app" % ( - instance, app_attr["namespacePrefix"], app_name - ) - self.window.run_command("login_to_sfdc", {"startURL": start_url}) - -class RetrieveLightingFromServer(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(RetrieveLightingFromServer, self).__init__(*args, **kwargs) - - def run(self, dirs): - processor.handle_retrieve_package( - self.types, - self.settings["workspace"], - ignore_package_xml=True - ) - - def is_visible(self, dirs): - if len(dirs) == 0: return False - self.settings = context.get_settings() - self.types = {} - for _dir in dirs: - if os.path.isfile(_dir): continue - base, _name = os.path.split(_dir) - base, _folder = os.path.split(base) - - # Check Metadata Type - if _folder != "aura": continue - - # Check Project Name - pn = self.settings["default_project_name"] - if pn not in _dir: continue - - if "AuraDefinitionBundle" in self.types: - self.types["AuraDefinitionBundle"].append(_name) - else: - self.types["AuraDefinitionBundle"] = [_name] - - # Check whether any aura components are chosen - if not self.types: return False - - return True - -class DestructLightingFromServer(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(DestructLightingFromServer, self).__init__(*args, **kwargs) - - def run(self, dirs): - confirm = sublime.ok_cancel_dialog("Are you sure you really want to continue?") - if not confirm: return - processor.handle_destructive_files(dirs, ignore_folder=False) - - def is_visible(self, dirs): - if len(dirs) == 0: return False - self.settings = context.get_settings() - for _dir in dirs: - base, name = os.path.split(_dir) - base, _folder = os.path.split(base) - if _folder != "aura": return False - if not util.check_enabled(_dir, check_cache=False): - return False - - return True - -class CreateLightingElement(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingElement, self).__init__(*args, **kwargs) - - def run(self, dirs, element=""): - """ element: Component, Controller, Helper, Style, Documentation, Render - """ - - # Get template attribute - template_settings = sublime.load_settings("template.sublime-settings") - template = template_settings.get("template").get("AuraEelement").get(element) - extension = template["extension"] - body = template["body"] - - # JS Component is different with others - element_name = "%s%s%s" % ( - self.aura_name, - element if extension == ".js" else "", - extension - ) - - # Combine Aura element component name - element_file = os.path.join(self._dir, element_name) - - # If element file is already exist, just alert - if os.path.isfile(element_file): - Printer.get('error').write(element_name+" is already exist") - return - - # Create Aura Element file - with open(element_file, "w") as fp: - fp.write(body) - - # If created succeed, just open it and refresh project - window = sublime.active_window() - window.open_file(element_file) - window.run_command("refresh_folder_list") - - # Deploy Aura to server - self.window.run_command("deploy_lighting_to_server", { - "dirs": [self._dir], - "switch_project": False - }) - - def is_visible(self, dirs, element=""): - if not dirs or len(dirs) != 1: return False - self._dir = dirs[0] - - # Check whether project is the active one - settings = context.get_settings() - if settings["default_project_name"] not in self._dir: - return False - - base, self.aura_name = os.path.split(self._dir) - base, meta_type = os.path.split(base) - if meta_type != "aura": return False - - lighting_extensions = [] - for dirpath, dirnames, filenames in os.walk(self._dir): - for filename in filenames: - extension = filename[filename.find("."):] - lighting_extensions.append(extension) - - # Just Component and Application can have child elements - if ".cmp" in lighting_extensions or ".app" in lighting_extensions: - return True - - return False - -class CreateLightingDefinition(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingDefinition, self).__init__(*args, **kwargs) - - def run(self, _type=""): - self._type = _type - self.window.show_input_panel("Please Input Lighting Name: ", - "", self.on_input, None, None) - - def on_input(self, lighting_name): - # Create component to local according to user input - if not re.match('^[a-zA-Z]+\\w+$', lighting_name): - message = 'Invalid format, do you want to try again?' - if not sublime.ok_cancel_dialog(message): return - self.window.show_input_panel("Please Input Lighting Name: ", - "", self.on_input, None, None) - return - - # Get template attribute - template_settings = sublime.load_settings("template.sublime-settings") - template = template_settings.get("template").get("Aura").get(self._type) - - # Build dir for new lighting component - settings = context.get_settings() - component_dir = os.path.join(settings["workspace"], "src", "aura", lighting_name) - if not os.path.exists(component_dir): - os.makedirs(component_dir) - else: - message = lighting_name+" is already exist, do you want to try again?" - if not sublime.ok_cancel_dialog(message, "Try Again?"): return - self.window.show_input_panel("Please Input Lighting Name: ", - "", self.on_input, None, None) - return - - lihghting_file = os.path.join(component_dir, lighting_name+template["extension"]) - - # Create Aura lighting file - with open(lihghting_file, "w") as fp: - fp.write(template["body"]) - - # If created succeed, just open it and refresh project - window = sublime.active_window() - window.open_file(lihghting_file) - window.run_command("refresh_folder_list") - - # Deploy Aura to server - self.window.run_command("deploy_lighting_to_server", { - "dirs": [component_dir], - "switch_project": False - }) - -class CreateLightingApplication(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingApplication, self).__init__(*args, **kwargs) - - def run(self): - self.window.run_command("create_lighting_definition", { - "_type": "Application" - }) - - def is_enabled(self): - return util.check_action_enabled() - -class CreateLightingComponent(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingComponent, self).__init__(*args, **kwargs) - - def run(self): - self.window.run_command("create_lighting_definition", { - "_type": "Component" - }) - - def is_enabled(self): - return util.check_action_enabled() - -class CreateLightingInterface(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingInterface, self).__init__(*args, **kwargs) - - def run(self): - self.window.run_command("create_lighting_definition", { - "_type": "Interface" - }) - - def is_enabled(self): - return util.check_action_enabled() - -class CreateLightingEvent(sublime_plugin.WindowCommand): - def __init__(self, *args, **kwargs): - super(CreateLightingEvent, self).__init__(*args, **kwargs) - - def run(self): - self.window.run_command("create_lighting_definition", { - "_type": "Event" - }) - - def is_enabled(self): - return util.check_action_enabled() - class SaveToServer(sublime_plugin.TextCommand): def run(self, edit, is_check_only=False): # Automatically save current file if dirty @@ -1938,12 +1684,33 @@ def run(self, edit, is_check_only=False): def is_enabled(self): if not self.view or not self.view.file_name(): return False - _folder = util.get_metadata_folder(self.view.file_name()) - if _folder not in ["classes", "components", "pages", "triggers"]: + attributes = util.get_file_attributes(self.view.file_name()) + if attributes["metadata_folder"] not in ["classes", "components", "pages", "triggers"]: return False return util.check_enabled(self.view.file_name()) + def is_visible(self): + return self.is_enabled() + +class ViewFileAttributes(sublime_plugin.TextCommand): + def run(self, edit): + view = sublime.active_window().new_file() + view.run_command("new_view", { + "name": self.cname, + "input": json.dumps(self.component_attribute, indent=4) + }) + + def is_enabled(self): + if not self.view or not self.view.file_name(): return False + self.file_name = self.view.file_name() + self.settings = context.get_settings() + self.component_attribute, self.cname = util.get_component_attribute(self.file_name) + if not self.component_attribute: + return False + + return True + class SwitchProjectCommand(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(SwitchProjectCommand, self).__init__(*args, **kwargs) @@ -2120,6 +1887,9 @@ def is_enabled(self): return True + def is_visible(self): + return self.is_enabled() + class RefreshFilesFromServer(sublime_plugin.WindowCommand): def __init__(self, *args, **kwargs): super(RefreshFilesFromServer, self).__init__(*args, **kwargs) diff --git a/messages.json b/messages.json index 67335de..d42bb22 100644 --- a/messages.json +++ b/messages.json @@ -1,17 +1,8 @@ { - "3.0.0": "config/messages/3.0.0.md", - "3.0.1": "config/messages/3.0.1.md", - "3.0.2": "config/messages/3.0.2.md", - "3.0.3": "config/messages/3.0.3.md", - "3.0.4": "config/messages/3.0.4.md", - "3.0.5": "config/messages/3.0.5.md", - "3.0.6": "config/messages/3.0.6.md", - "3.0.7": "config/messages/3.0.7.md", - "3.0.8": "config/messages/3.0.8.md", - "3.0.9": "config/messages/3.0.9.md", "3.1.0": "config/messages/3.1.0.md", "3.1.1": "config/messages/3.1.1.md", "3.1.2": "config/messages/3.1.2.md", "3.1.3": "config/messages/3.1.3.md", + "3.1.4": "config/messages/3.1.4.md", "install": "config/messages/install.txt" } \ No newline at end of file diff --git a/package.py b/package.py index 2a55382..11551a2 100644 --- a/package.py +++ b/package.py @@ -195,6 +195,17 @@ def __init__(self, *args, **kwargs): super(BuildPackageXml, self).__init__(*args, **kwargs) def run(self): + if not hasattr(self, "filters"): + sublime.active_window().show_input_panel( + "Input filters for members separated with comma: ", + "", self.on_input_filters, None, None + ) + else: + self.on_input_filters(",".join(self.filters)) + + def on_input_filters(self, filters): + self.filters = filters.split(",") if filters else [] + package_cache = os.path.join(self.settings["workspace"], ".config", "package.json") if not os.path.exists(package_cache): return self.window.run_command("reload_project_cache", { @@ -203,11 +214,11 @@ def run(self): self.package = json.loads(open(package_cache).read()) - view = util.get_view_by_name("package.xml") - types = view.settings().get("types") if view else {} - if not types: types = {} - + view = self.window.active_view() + types = view.settings().get("types", {}) if view else {} + self.members = [] + self.matched_package = {} for metadata_object in sorted(self.package.keys()): if not self.package[metadata_object]: continue if metadata_object in types: @@ -216,13 +227,34 @@ def run(self): display = "[x]" + metadata_object self.members.append(display) + matched_members = [] for mem in self.package[metadata_object]: - if metadata_object in types and mem in types[metadata_object]: + if self.filters and not self.is_filter_match(mem): + continue + matched_members.append(mem) + + if mem in types.get(metadata_object, []): mem = "[√]" + metadata_object + " => " + mem else: mem = "[x]" + metadata_object + " => " + mem self.members.append(" %s" % mem) + # If no matched member, just skip + if not matched_members: + self.members.remove(display) + continue + + self.matched_package[metadata_object] = matched_members + + if not self.members: + message = "No matched member found by filters('%s'), do you want to retry" % ",".join(self.filters) + if sublime.ok_cancel_dialog(message, "Retry"): + return sublime.active_window().show_input_panel( + "Input filters for members separated with comma: ", + "", self.on_input_filters, None, None + ) + return + # Get the last subscribe index selected_index = view.settings().get("selected_index") if view else 0 if not selected_index: selected_index = 0 @@ -230,8 +262,21 @@ def run(self): self.window.show_quick_panel(self.members, self.on_done, sublime.MONOSPACE_FONT, selected_index) + def is_filter_match(self, member): + isFilterMatch = False + for _filter in self.filters: + _filter = _filter.strip() + if _filter.lower() in member.lower(): + isFilterMatch = True + break + + return isFilterMatch + def on_done(self, index): - if index == -1: return + if index == -1: + del self.filters + return + chosen_element = self.members[index] if " => " in chosen_element: @@ -243,8 +288,8 @@ def on_done(self, index): is_chosen = "[√]" in chosen_metadata_object chosen_metadata_object = chosen_metadata_object[3:] - view = util.get_view_by_name("package.xml") - if not view: + view = self.window.active_view() + if not view or not view.settings().has("types"): view = self.window.new_file() view.set_syntax_file("Packages/XML/xml.tmLanguage") view.run_command("new_view", { @@ -253,16 +298,14 @@ def on_done(self, index): }) view.settings().set("selected_index", index) self.window.focus_view(view) - - types = view.settings().get("types") - if not types: types = {} + types = view.settings().get("types", {}) if not chosen_member: if not is_chosen: - types[chosen_metadata_object] = self.package[chosen_metadata_object] + types[chosen_metadata_object] = self.matched_package[chosen_metadata_object] else: - if len(types[chosen_metadata_object]) != len(self.package[chosen_metadata_object]): - types[chosen_metadata_object] = self.package[chosen_metadata_object] + if len(types[chosen_metadata_object]) != len(self.matched_package[chosen_metadata_object]): + types[chosen_metadata_object] = self.matched_package[chosen_metadata_object] else: del types[chosen_metadata_object] elif chosen_metadata_object in types: @@ -307,7 +350,7 @@ def on_done(self, index): "input": util.format_xml(self.package_xml_content).decode("UTF-8") }) - sublime.set_timeout(lambda:self.window.run_command("build_package_xml"), 10) + sublime.set_timeout(lambda:self.on_input_filters(",".join(self.filters)), 10) def is_enabled(self): self.settings = context.get_settings() diff --git a/processor.py b/processor.py index 1f52e68..556b269 100644 --- a/processor.py +++ b/processor.py @@ -154,17 +154,18 @@ def handle_thread(thread, timeout): ThreadProgress(api, thread, "Login to %s" % default_project_name, default_project_name + " Login Succeed") -def handle_view_code_coverage(component_name, component_attribute, body, timeout=120): +def handle_view_code_coverage(component_name, component_id, body, timeout=120): def handle_thread(thread, timeout): if thread.is_alive(): sublime.set_timeout(lambda: handle_thread(thread, timeout), timeout) return result = api.result - if not result["success"]: return + if not result["success"]: + return if result["totalSize"] == 0: - Printer.get("log").write("No code coverage") + Printer.get("log").write("There is no available code coverage") return # Populate the coverage info from server @@ -174,7 +175,7 @@ def handle_thread(thread, timeout): uncovered_lines_count = len(uncovered_lines) total_lines_count = covered_lines_count + uncovered_lines_count if total_lines_count == 0: - Printer.get("log").write("No Code Coverage") + Printer.get("log").write("There is no available code coverage") return coverage_percent = covered_lines_count / total_lines_count * 100 @@ -207,7 +208,7 @@ def handle_thread(thread, timeout): settings = context.get_settings() api = ToolingApi(settings) query = "SELECT Coverage FROM ApexCodeCoverageAggregate " +\ - "WHERE ApexClassOrTriggerId = '{0}'".format(component_attribute["id"]) + "WHERE ApexClassOrTriggerId = '{0}'".format(component_id) thread = threading.Thread(target=api.query, args=(query, True, )) thread.start() ThreadProgress(api, thread, "View Code Coverage of " + component_name, @@ -960,7 +961,7 @@ def handle_thread(thread, timeout): if "success" in result and not result["success"]: return if not result: - Printer.get("error").write("%s is not a test class" % class_name) + return Printer.get("error").write("%s is not a test class" % class_name) # No error, just display log in a new view test_result = util.parse_test_result(result) @@ -1008,46 +1009,59 @@ def handle_code_coverage_thread(thread, view, timeout): ThreadProgress(api, thread, "Run Test Class " + class_name, "Run Test for " + class_name + " Succeed") handle_thread(thread, timeout) -def handle_run_sync_test_classes(class_names, timeout=120): - def handle_new_view_thread(thread, timeout): +def handle_run_sync_test(class_names, timeout=120): + def handle_thread(thread, timeout): if thread.is_alive(): - sublime.set_timeout(lambda: handle_new_view_thread(thread, timeout), timeout) - return - elif not api.result: + sublime.set_timeout(lambda: handle_thread(thread, timeout), timeout) return # If succeed result = api.result - pprint.pprint(result) - pprint.pprint(util.parse_code_coverage(result)) + if "success" in result and not result["success"]: + return - settings = context.get_settings() - api = ToolingApi(settings) - thread = threading.Thread(target=api.run_tests_synchronous, args=(class_names, )) - thread.start() - wait_message = 'Run Sync Test Classes for Specified Test Class' - ThreadProgress(api, thread, wait_message, wait_message + ' Succeed') - handle_new_view_thread(thread, timeout) + if result["numTestsRun"] == 0: + return Printer.get("error").write("There is no available test to run") -def handle_run_async_test_classes(class_ids, timeout=120): - def handle_new_view_thread(thread, timeout): - if thread.is_alive(): - sublime.set_timeout(lambda: handle_new_view_thread(thread, timeout), timeout) - return - elif not api.result: - return + view = sublime.active_window().new_file() + view.run_command("new_view", { + "name": ",".join(class_names) + " Coverage", + "input": util.parse_sync_test_coverage(result) + }) - # If succeed - result = api.result - pprint.pprint(result) + if settings["debug_mode"]: + view = sublime.active_window().new_file() + view.run_command("new_view", { + "name": ",".join(class_names) + " Debug Mode", + "input": json.dumps(result, indent=4) + }) + + # Keep the coverage to local cache + codeCoverages = result["codeCoverage"] + cache_dir = os.path.join(settings["workspace"], ".config") + cache_file = os.path.join(cache_dir, "coverage.json") + + coverages = {} + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + elif os.path.isfile(cache_file): + coverages = json.loads(open(cache_file).read()) + + # Upsert exist code coverage info + for codeCoverage in codeCoverages: + lowerName = codeCoverage["name"].lower() + coverages[lowerName] = codeCoverage + + with open(cache_file, "w") as fp: + fp.write(json.dumps(coverages, indent=4)) settings = context.get_settings() api = ToolingApi(settings) - thread = threading.Thread(target=api.run_tests_asynchronous, args=(class_ids, )) + thread = threading.Thread(target=api.run_tests_synchronous, args=(class_names, )) thread.start() - wait_message = 'Run Sync Test Classes for Specified Test Class' + wait_message = 'Running Sync Test for %s' % ",".join(class_names) ThreadProgress(api, thread, wait_message, wait_message + ' Succeed') - handle_new_view_thread(thread, timeout) + handle_thread(thread, timeout) def handle_generate_sobject_soql(sobject, filter, timeout=120): def handle_new_view_thread(thread, timeout): @@ -1116,6 +1130,18 @@ def handle_new_view_thread(thread, timeout): handle_new_view_thread(thread, timeout) def handle_export_specified_workbooks(sobjects, timeout=120): + # settings = context.get_settings() + # api = ToolingApi(settings) + # threads = [] + + # mcc = settings["maximum_concurrent_connections"] + # chunked_sobjects = util.list_chunks(sobjects, math.ceil(len(sobjects) / mcc)) + + # for cs in chunked_sobjects: + # thread = threading.Thread(target=api.generate_workbook, args=(cs, )) + # threads.append(thread) + # thread.start() + settings = context.get_settings() api = ToolingApi(settings) threads = [] @@ -1137,10 +1163,16 @@ def handle_thread(thread, timeout): if not api.result["success"]: return # If succeed - sobjects_describe = api.result["sobjects"] - for sd in sobjects_describe: + sobjects = [] + for sd in api.result["sobjects"]: if "name" not in sd: continue - thread = threading.Thread(target=api.generate_workbook, args=(sd["name"], )) + sobjects.append(sd["name"]) + + mcc = settings["maximum_concurrent_connections"] + chunked_sobjects = util.list_chunks(sobjects, math.ceil(len(sobjects) / mcc)) + + for sobjects in chunked_sobjects: + thread = threading.Thread(target=api.generate_workbook, args=(sobjects, )) thread.start() settings = context.get_settings() @@ -1180,7 +1212,7 @@ def handle_thread(thread, timeout): # Extract the zipFile to extract_to thread = threading.Thread(target=util.extract_encoded_zipfile, - args=(result["zipFile"], extract_to, )) + args=(result["zipFile"], extract_to,)) thread.start() # Apex Code Cache @@ -1326,7 +1358,7 @@ def handle_thread(thread, timeout): thread = threading.Thread(target=api.prepare_members, args=(types, True, )) thread.start() handle_thread(thread, timeout) - ThreadProgress(api, thread, "Reload Project Cache", "Reload Project Cache Succeed") + ThreadProgress(api, thread, "Reloading Project Cache", "Reload Project Cache Succeed") def handle_retrieve_package(types, extract_to, source_org=None, ignore_package_xml=False, timeout=120): def handle_thread(thread, timeout): @@ -1353,7 +1385,8 @@ def handle_thread(thread, timeout): thread = threading.Thread(target=api.retrieve, args=({"types": types}, )) thread.start() handle_thread(thread, timeout) - ThreadProgress(api, thread, "Retrieve Metadata", "Retrieve Metadata Succeed") + ThreadProgress(api, thread, "Retrieve File From Server", + "Retrieve File From Server Succeed") def handle_save_to_server(file_name, is_check_only=False, timeout=120): def handle_thread(thread, timeout): @@ -1525,21 +1558,22 @@ def handle_thread(thread, timeout): # Save it to component.sublime-settings s = sublime.load_settings(COMPONENT_METADATA_SETTINGS) username = settings["username"] - components_dict = s.get(username) + components_dict = s.get(username, {}) # Prevent exception for creating component if no component in org if component_type not in components_dict: components_dict = {component_type : {}} # Build components dict - components_dict[component_type][component_name.lower()] = { + lower_name = component_name.lower() + components_dict[component_type][fullName.lower()] = { "id": component_id, "name": component_name, "url": post_url + "/" + component_id, "body": markup_or_body, "extension": extension, "type": component_type, - "is_test": False + "is_test": lower_name.startswith("test") or lower_name.endswith("test") } s.set(username, components_dict) @@ -1564,14 +1598,15 @@ def handle_thread(thread, timeout): # Generate new meta.xml file with open(file_name+"-meta.xml", "w") as fp: fp.write(meta_file_content) - + settings = context.get_settings() api = ToolingApi(settings) post_url = "/sobjects/" + component_type thread = threading.Thread(target=api.post, args=(post_url, data, )) thread.start() - ThreadProgress(api, thread, "Creating Component " + component_name, - "Creating Component " + component_name + " Succeed") + fullName = os.path.basename(file_name) + ThreadProgress(api, thread, "Creating Component %s" % fullName, + "Creating Component %s Succeed" % fullName) handle_thread(thread, timeout) def handle_refresh_static_resource(component_attribute, file_name, timeout=120): diff --git a/salesforce/api/metadata.py b/salesforce/api/metadata.py index 0106887..f4041ae 100644 --- a/salesforce/api/metadata.py +++ b/salesforce/api/metadata.py @@ -479,7 +479,6 @@ def list_package(self, _types): return [] result = xmltodict.parse(response.content) - pprint.pprint(result) result = result["soapenv:Envelope"]["soapenv:Body"]["listMetadataResponse"] if not result or "result" not in result: return [] diff --git a/salesforce/api/tooling.py b/salesforce/api/tooling.py index b6383f4..b8a7122 100644 --- a/salesforce/api/tooling.py +++ b/salesforce/api/tooling.py @@ -857,24 +857,26 @@ def run_test(self, class_id): # Combine these two result self.result = result - def generate_workbook(self, sobject): + def generate_workbook(self, sobjects): """ Generate CSV for Sobject Workbook Arguments: * sobject -- sobject name """ - result = self.describe_sobject(sobject) - # Exception Process - if not result["success"]: - self.result = result - return result + for sobject in sobjects: + result = self.describe_sobject(sobject) + + # Exception Process + if not result["success"]: + self.result = result + return result - workspace = self.settings.get("workspace") - outputdir = util.generate_workbook(result, workspace, - self.settings.get("workbook_field_describe_columns"))+"/"+sobject+".csv" - print (sobject + " workbook outputdir: " + outputdir) + workspace = self.settings.get("workspace") + outputdir = util.generate_workbook(result, workspace, + self.settings.get("workbook_field_describe_columns"))+"/"+sobject+".csv" + print (sobject + " workbook outputdir: " + outputdir) def save_to_server(self, component_attribute, body, is_check_only, check_save_conflict=True): """ This method contains 5 steps: @@ -1063,6 +1065,8 @@ def save_to_server(self, component_attribute, body, is_check_only, check_save_co # Fix issue github://haoide/issue#7 if "problem" in return_result: problem = return_result["problem"] + if not problem: + problem = "Unknown problem, please try to use `Deploy To This Server` command" if isinstance(problem, list): problem = "\n".join(problem) return_result["problem"] = urllib.parse.unquote( diff --git a/util.py b/util.py index 86f0aba..5e28211 100644 --- a/util.py +++ b/util.py @@ -30,6 +30,24 @@ def get_described_metadata(username): st = sublime.load_settings("metadata.sublime-settings") return st.get(username) +def get_instance(settings): + """ Get instance by instance_url + + Return: + * instance -- instance of active project, for example, + if instance_url is https://ap1.salesforce.com, + instance will be `ap1`, + if instance_url is https://company-name.cs18.my.salesforce.com + instance will be `company-name.cs18.my` + """ + + session = get_session_info(settings) + instance_url = session["instance_url"] + base_url = re.compile("//[\s\S]+?.salesforce.com").search(instance_url).group() + instance = base_url[2:base_url.find(".salesforce.com")] + + return instance + def get_session_info(settings): """ Get Session Info @@ -57,6 +75,57 @@ def get_package_info(settings): return package +def view_coverage(name, file_name, body): + settings = context.get_settings() + cache_file = os.path.join(settings["workspace"], ".config", "coverage.json") + coverages = {} + if os.path.isfile(cache_file): + coverages = json.loads(open(cache_file).read()) + coverage = coverages.get(name.lower(), {}) + + if not coverage: + return Printer.get("error").write("No code coverage cache, " +\ + "please execute `Run Sync Test` on related test class before view code coverage") + + numLocationsNotCovered = coverage["numLocationsNotCovered"] + numLocations = coverage["numLocations"] + numLocationsCovered = numLocations - numLocationsNotCovered + linesNotCovered = [l["line"] for l in coverage["locationsNotCovered"]] + if numLocations == 0: + return Printer.get("error").write("There is no code coverage") + + # Append coverage statistic info + coverage_statistic = "%s Coverage: %.2f%%(%s/%s)" % ( + name, numLocationsCovered / numLocations * 100, + numLocationsCovered, numLocations + ) + + # If has coverage, just add coverage info to new view + view = sublime.active_window().new_file() + view.run_command("new_view", { + "name": coverage_statistic, + "input": body + }) + + # Calculate line coverage + split_lines = view.lines(sublime.Region(0, view.size())) + uncovered_region = [] + covered_region = [] + for region in split_lines: + # The first four Lines are the coverage info + line = view.rowcol(region.begin() + 1)[0] + 1 + if line in linesNotCovered: + uncovered_region.append(region) + else: + covered_region.append(region) + + # Append body with uncovered line + view.add_regions("numLocationsNotCovered", uncovered_region, "invalid", "dot", + sublime.DRAW_SOLID_UNDERLINE | sublime.DRAW_EMPTY_AS_OVERWRITE) + + view.add_regions("numLocationsCovered", covered_region, "comment", "cross", + sublime.DRAW_SOLID_UNDERLINE | sublime.DRAW_EMPTY_AS_OVERWRITE) + def get_local_timezone_offset(): """ Return the timezone offset of local time with GMT standard @@ -159,7 +228,7 @@ def populate_lighting_applications(): aura_attributes = {} aura_cache = component_settings.get(username).get("AuraDefinitionBundle") for name in aura_cache: - aura_name, element_name = aura_cache[name]["name"].split("/") + aura_name, element_name = aura_cache[name]["fullName"].split("/") if element_name.endswith(".app"): aura_attributes[aura_name] = aura_cache[name] @@ -702,7 +771,6 @@ def get_view_by_name(view_name): view = None for win in sublime.windows(): for v in win.views(): - if not v.name(): continue if v.name() == view_name: view = v @@ -817,32 +885,27 @@ def build_package_dict(files, ignore_folder=True): if f.endswith("-meta.xml"): continue - # Get xml_name and code name - if ignore_folder: - attributes = get_file_attributes(f) - print (attributes) - mo = settings[attributes["metadata_folder"]] - metadata_object = mo["xmlName"] - file_dict = { - "name": attributes["name"], - "dir": f, - "folder": attributes["metadata_folder"], - "extension": "."+attributes["extension"] - } - - if mo["inFolder"] == "true": - file_dict["name"] = "%s/%s" % ( - attributes["folder"], attributes["name"] - ) - else: - attributes = get_file_attributes(f) - mo = settings[attributes["metadata_folder"]] - file_dict = { - "name": attributes["name"], - "dir": f, - "folder": attributes["metadata_folder"], - "extension": "."+attributes["extension"] - } + # If ignore_folder is true and f is folder + attributes = get_file_attributes(f) + metadata_folder = attributes["metadata_folder"] + mo = settings[metadata_folder] + metadata_object = mo["xmlName"] + file_dict = { + "name": attributes["name"], + "metadata_name": attributes["name"], + "dir": f, + "folder": attributes["folder"] if "folder" in attributes else "", + "metadata_folder": attributes["metadata_folder"], + "extension": attributes["extension"] + } + + if mo["inFolder"] == "true": + file_dict["metadata_name"] = "%s/%s" % ( + attributes["folder"], attributes["name"] + ) + + if metadata_folder == "aura": + file_dict["metadata_name"] = "%s" % attributes["folder"] # Build dict if metadata_object in package_dict: @@ -870,7 +933,7 @@ def build_package_xml(settings, package_dict): for meta_type in package_dict: members = [] for f in package_dict[meta_type]: - members.append("%s" % f["name"]) + members.append("%s" % f["metadata_name"]) types.append(""" @@ -901,12 +964,14 @@ def build_destructive_package(files, ignore_folder=True): # Build destructiveChanges.xml destructive_xml_content = build_package_xml(settings, package_dict) destructive_xml_path = workspace+"/destructiveChanges.xml" - open(destructive_xml_path, "wb").write(destructive_xml_content.encode("utf-8")) + with open(destructive_xml_path, "wb") as fp: + fp.write(destructive_xml_content.encode("utf-8")) # Build package.xml package_xml_content = build_package_xml(settings, {}) package_xml_path = workspace+"/package.xml" - open(package_xml_path, "wb").write(package_xml_content.encode("utf-8")) + with open(package_xml_path, "wb") as fp: + fp.write(package_xml_content.encode("utf-8")) # Create temp zipFile zipfile_path = workspace + "/test.zip" @@ -945,12 +1010,18 @@ def build_deploy_package(files): # Add files to zip for meta_type in package_dict: for f in package_dict[meta_type]: - zf.write(f["dir"], "%s/%s%s" % (f["folder"], f["name"], f["extension"])) + write_to = ( + f["metadata_folder"], + ("/" + f["folder"]) if f["folder"] else "", + f["name"], + f["extension"] + ) + zf.write(f["dir"], "%s%s/%s.%s" % write_to) # If -meta.xml is exist, add it to folder met_xml = f["dir"] + "-meta.xml" if os.path.isfile(met_xml): - zf.write(met_xml, "%s/%s%s" % (f["folder"], f["name"], f["extension"]+"-meta.xml")) + zf.write(met_xml, "%s%s/%s.%s-meta.xml" % write_to) # Prepare package XML content package_xml_content = build_package_xml(settings, package_dict) @@ -962,7 +1033,8 @@ def build_deploy_package(files): if not os.path.exists(package_xml_dir): os.makedirs(package_xml_dir) time_stamp = time.strftime("%Y%m%d%H%M", time.localtime(time.time())) package_xml_dir = package_xml_dir + "/package-%s.xml" % time_stamp - open(package_xml_dir, "wb").write(package_xml_content) + with open(package_xml_dir, "wb") as fp: + fp.write(package_xml_content) zf.write(package_xml_dir, "package.xml") except Exception as ex: if settings["debug_mode"]: @@ -1129,8 +1201,12 @@ def extract_zipfile(zipfile_path, extract_to): def extract_file(zipfile_path, extract_to, ignore_package_xml=False): zfile = zipfile.ZipFile(zipfile_path, 'r') for filename in zfile.namelist(): - if filename.endswith('/'): continue - if ignore_package_xml and filename == "unpackaged/package.xml": continue + if filename.endswith('/'): + continue + + if ignore_package_xml and filename == "unpackaged/package.xml": + continue + if filename.startswith("unpackaged"): f = os.path.join(extract_to, filename.replace("unpackaged", "src")) else: @@ -1225,8 +1301,7 @@ def reload_apex_code_cache(file_properties, settings=None): "ApexTrigger": "Body", "StaticResource": "Body", "ApexPage": "Markup", - "ApexComponent": "Markup", - "AuraDefinitionBundle": "" + "ApexComponent": "Markup" } # If the package only contains `package.xml` @@ -1271,7 +1346,7 @@ def reload_apex_code_cache(file_properties, settings=None): cl = name.lower() attrs["is_test"] = cl.startswith("test") or cl.endswith("test") - components_attr[name.lower()] = attrs + components_attr[base_name.lower()] = attrs all_components_attr[metdata_object] = components_attr component_settings = sublime.load_settings(context.COMPONENT_METADATA_SETTINGS) @@ -1565,6 +1640,80 @@ def parse_code_coverage(result): return message.SEPRATE.format(code_coverage_desc + columns + "\n"*2 + code_coverage) +def parse_sync_test_coverage(result): + successes = result["successes"] + failures = result["failures"] + codeCoverage = result["codeCoverage"] + + allrows = [] + if result["successes"]: + for success in result["successes"]: + allrows.append("~" * 80) + success_row = [] + success_row.append("% 30s %-30s " % ("ClassName: ", success["name"])) + success_row.append("% 30s %-30s " % ("MethodName: ", success["methodName"])) + success_row.append("% 30s %-30s " % ("SeeAllData: ", success["seeAllData"])) + success_row.append("% 30s %-30s " % ("Pass/Fail: ", "Pass")) + success_row.append("% 30s %-30s " % ("Time: ", success["time"])) + allrows.append("\n".join(success_row)) + + if result["failures"]: + for failure in result["failures"]: + allrows.append("~" * 80) + failure_row = [] + failure_row.append("% 30s %-30s " % ("ClassName: ", failure["name"])) + failure_row.append("% 30s %-30s " % ("MethodName: ", failure["methodName"])) + failure_row.append("% 30s %-30s " % ("SeeAllData: ", failure["seeAllData"])) + failure_row.append("% 30s %-30s " % ("Pass/Fail: ", "Fail")) + failure_row.append("% 30s %-30s " % ("StackTrace: ", failure["stackTrace"])) + failure_row.append("% 30s %-30s " % ("Message: ", failure["message"])) + failure_row.append("% 30s %-30s " % ("Time: ", failure["time"])) + allrows.append("\n".join(failure_row)) + + allrows.append("~" * 80) + allrows.append("Follow the instruction as below, you can quickly view code coverage,") + allrows.append(" * Put focus on code name, hold down 'alt' and triple-click the 'Left Mouse'") + + header_width = { + "Type": 15, "Name": 50, "Percent": 10, "Lines": 10 + } + columns = [] + for column in ["Type", "Name", "Percent", "Lines"]: + columns.append("%-*s" % (header_width[column], column)) + + coverageRows = [] + coverageRows.append("~" * 80) + coverageRows.append("".join(columns)) + coverageRows.append("~" * 80) + codeCoverage = sorted(result["codeCoverage"], reverse=True, + key=lambda k : (k["numLocations"] - k['numLocationsNotCovered']) / k["numLocations"]) + for coverage in codeCoverage: + coverageRow = [] + coverageRow.append("%-*s" % (header_width["Type"], coverage["type"])) + coverageRow.append("%-*s" % (header_width["Name"], coverage["name"])) + + # Calculate coverage percent + numLocationsNotCovered = coverage["numLocationsNotCovered"] + numLocations = coverage["numLocations"] + numLocationsCovered = numLocations - numLocationsNotCovered + percent = numLocationsCovered / numLocations * 100 if numLocations != 0 else 0 + coverageRow.append("%-*s" % ( + header_width["Percent"], + "%.2f%%" % percent + )) + coverageRow.append("%-*s" % ( + header_width["Lines"], "%s/%s" % ( + numLocationsCovered, + numLocations + ) + )) + + coverageRows.append("".join(coverageRow)) + + allrows.append("\n".join(coverageRows)) + + return "\n".join(allrows) + def parse_test_result(test_result): """ format test result as specified format @@ -1578,7 +1727,7 @@ def parse_test_result(test_result): test_result_content = "" class_name = "" for record in test_result: - test_result_content += "-" * 100 + "\n" + test_result_content += "-" * 80 + "\n" test_result_content += "% 30s " % "MethodName: " test_result_content += "%-30s" % none_value(record["MethodName"]) + "\n" test_result_content += "% 30s " % "TestTimestamp: " @@ -1596,7 +1745,9 @@ def parse_test_result(test_result): return_result = class_name + test_result_desc + test_result_content[:-1] # Parse Debug Log Part - debug_log_desc = message.SEPRATE.format("You can choose the LogId and view log detail in Sublime or Salesforce by context menu") + info = "You can choose the LogId and view log detail " +\ + "in Sublime or Salesforce by context menu" + debug_log_desc = message.SEPRATE.format(info) debug_log_content = "LogId: " if len(test_result) > 0 and test_result[0]["ApexLogId"] != None: debug_log_content += test_result[0]["ApexLogId"] @@ -2271,6 +2422,7 @@ def get_file_attributes(file_name): name, extension = fullName.split(".") else: name, extension = fullName, "" + attributes["fullName"] = fullName attributes["name"] = name attributes["extension"] = extension @@ -2279,6 +2431,10 @@ def get_file_attributes(file_name): if metafolder_or_src == "src": attributes["metadata_folder"] = folder + # If we choose folder name of an aura element + # actually, its name is also its folder name + if not os.path.isfile(file_name): + attributes["folder"] = name else: attributes["folder"] = folder attributes["metadata_folder"] = metafolder_or_src @@ -2327,20 +2483,26 @@ def get_component_attribute(file_name): # Check whether current file is subscribed component attributes = get_file_attributes(file_name) metadata_folder = attributes["metadata_folder"] - component_name = attributes["name"] + name = attributes["name"] + fullName = attributes["fullName"] if metadata_folder not in settings["all_metadata_folders"]: return None, None + # Check whether project of current file is active project + default_project_name = settings["default_project_name"] + if default_project_name.lower() not in file_name.lower(): + return None, None + xml_name = settings[metadata_folder]["xmlName"] username = settings["username"] components = sublime.load_settings(context.COMPONENT_METADATA_SETTINGS) try: - component_attribute = components.get(username)[xml_name][component_name.lower()] + component_attribute = components.get(username)[xml_name][fullName.lower()] except: - component_attribute, component_name = None, None + component_attribute, name = None, None # Return tuple - return (component_attribute, component_name) + return (component_attribute, name) def check_enabled(file_name, check_cache=True): """ @@ -2377,7 +2539,7 @@ def check_enabled(file_name, check_cache=True): username = settings["username"] component_attribute, component_name = get_component_attribute(file_name) if not component_attribute: - sublime.status_message("This component is not active component") + sublime.status_message("This component is not in active project") return False return True @@ -2394,17 +2556,22 @@ def display_active_project(view): ) view.set_status('default_project', display_message) -def switch_project(chosen_project): +def switch_project(target): """ Set the default project to the chosen one """ + # If target is same with current, just skip + settings = context.get_settings() + if target == settings["default_project_name"]: + return + s = sublime.load_settings(context.TOOLING_API_SETTINGS) projects = s.get("projects") # Set the chosen project as default and others as not default for project in projects: project_attr = projects[project] - if chosen_project == project: + if target == project: project_attr["default"] = True else: project_attr["default"] = False @@ -2417,14 +2584,12 @@ def switch_project(chosen_project): for window in sublime.windows(): if not window.views(): view = window.new_file() - view.set_status('default_project', - "Default Project => %s" % chosen_project) + view.set_status('default_project', "Default Project => %s" % target) window.focus_view(view) window.run_command("close") else: for view in window.views(): - view.set_status('default_project', - "Default Project => %s" % chosen_project) + view.set_status('default_project', "Default Project => %s" % target) def add_project_to_workspace(settings): """Add new project folder to workspace