import sublime import sublime_plugin import subprocess import os import re import shutil # import inspect SETTINGS_FILE = "StandardFormat.sublime-settings" # load settings settings = None platform = sublime.platform() global_path = os.environ["PATH"] local_path = "" selectors = {} SYNTAX_RE = re.compile(r'(?i)/([^/]+)\.(?:tmLanguage|sublime-syntax)$') def calculate_env(): """Generate environment based on global environment and local path""" global local_path env = dict(os.environ) env["PATH"] = local_path return env # Initialize a global path. Works on Unix only only right now def calculate_user_path(): """execute a user shell to return a real env path""" shell_command = settings.get("get_path_command") user_path = ( subprocess.check_output(shell_command) .decode("utf-8") .split('\n') ) maybe_path = [string for string in user_path if len(string) > 0 and string[0] == os.sep] return maybe_path def search_for_bin_paths(path, view_path_array=[]): dirname = path if os.path.isdir(path) else os.path.dirname(path) maybe_bin_path = os.path.join(dirname, 'node_modules', '.bin') found_path = os.path.isdir(maybe_bin_path) if found_path: view_path_array = view_path_array + [maybe_bin_path] return ( view_path_array if os.path.ismount(dirname) else search_for_bin_paths(os.path.dirname(dirname), view_path_array) ) def get_view_path(path_string): """ walk the fs from the current view to find node_modules/.bin """ project_path = search_for_bin_paths(path_string) return os.pathsep.join(project_path) def get_project_path(view): """ generate path of node_module/.bin for open project folders """ try: parent_window_folders = view.window().folders() except Exception: parent_window_folders = [] project_path = ( [get_view_path(folder) for folder in parent_window_folders] if parent_window_folders else [] ) return os.pathsep.join(list(filter(None, project_path))) def generate_search_path(view): """ run necessary work to generate a search path """ search_path = settings.get("PATH") if not isinstance(search_path, list): print( "StandardFormat: PATH in settings does not appear to be an array") search_path = [] if settings.get("use_view_path"): if view.file_name(): search_path = search_path + [get_view_path(view.file_name())] elif settings.get("use_project_path_fallback"): search_path = search_path + [get_project_path(view)] if settings.get("use_global_path"): search_path = search_path + [global_path] search_path = list(filter(None, search_path)) new_path = os.pathsep.join(search_path) return new_path def get_command(commands): """ Tries to validate and return a working formatting command """ for command in commands: if shutil.which(command[0], path=local_path): return command return None def print_status(global_path, search_path): command = get_command(settings.get("commands")) print("StandardFormat:") print(" global_path: {}".format(global_path)) print(" search_path: {}".format(search_path)) if command: print(" found {} at {}".format( command[0], shutil.which(command[0], path=local_path))) print(" command: {}".format(command)) if settings.get("check_version"): print( " {} version: {}" .format(command[0], command_version( shutil.which(command[0], path=local_path))) ) def plugin_loaded(): """ perform some work to set up env correctly. """ global global_path global local_path global settings settings = sublime.load_settings(SETTINGS_FILE) view = sublime.active_window().active_view() if platform != "windows": maybe_path = calculate_user_path() if len(maybe_path) > 0: global_path = maybe_path[0] search_path = generate_search_path(view) local_path = search_path print_status(global_path, search_path) class StandardFormatEventListener(sublime_plugin.EventListener): def on_pre_save(self, view): if settings.get("format_on_save") and is_javascript(view): os.chdir(os.path.dirname(view.file_name())) view.run_command("standard_format") def on_activated_async(self, view): global local_path search_path = generate_search_path(view) local_path = search_path if is_javascript(view) and settings.get("logging_on_view_change"): print_status(global_path, search_path) def is_javascript(view): """ Checks if the current view is JS or not. Used in pre_save event. """ # Check the file extension name = view.file_name() extensions = set(settings.get('extensions')) if name and os.path.splitext(name)[1][1:] in extensions: return True # If it has no name (?) or it's not a JS, check the syntax syntax = view.settings().get("syntax") if syntax and "javascript" in syntax.split("/")[-1].lower(): return True return False def standard_format(string, command): """ Uses subprocess to format a given string. """ startupinfo = None if platform == "windows": # Prevent cmd.exe window from popping up startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= ( subprocess.STARTF_USESTDHANDLES | subprocess.STARTF_USESHOWWINDOW ) startupinfo.wShowWindow = subprocess.SW_HIDE std = subprocess.Popen( command, env=calculate_env(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo ) std.stdin.write(bytes(string, 'UTF-8')) out, err = std.communicate() print(err) return out.decode("utf-8"), None def command_version(command): """ Uses subprocess to format a given string. """ startupinfo = None if platform == "windows": # Prevent cmd.exe window from popping up startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= ( subprocess.STARTF_USESTDHANDLES | subprocess.STARTF_USESHOWWINDOW ) startupinfo.wShowWindow = subprocess.SW_HIDE std = subprocess.Popen( [command, "--version"], env=calculate_env(), stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, startupinfo=startupinfo ) out, err = std.communicate() return out.decode("utf-8").replace("\r", ""), err class StandardFormatCommand(sublime_plugin.TextCommand): def run(self, edit): # Figure out if the desired formatter is available command = get_command(settings.get("commands")) if platform == "windows" and command is not None: # Windows hax command[0] = shutil.which(command[0], path=local_path) if not command: # Noop if we don't have the right tools. return None view = self.view view_syntax = view.settings().get('syntax', '') if view_syntax: match = SYNTAX_RE.search(view_syntax) if match: view_syntax = match.group(1).lower() else: view_syntax = '' if view_syntax and view_syntax in settings.get('extensions', []): selectors = settings.get("selectors") selector = selectors[view_syntax] else: selector = None os.chdir(os.path.dirname(view.file_name())) regions = [] # sel = view.sel() if selector: regions = view.find_by_selector(selector) else: allreg = sublime.Region(0, view.size()) regions.append(allreg) for region in regions: self.do_format(edit, region, view, command) def do_format(self, edit, region, view, command): s = view.substr(region) s, err = standard_format(s, command) if not err and len(s) > 0: view.replace(edit, region, s) elif err: loud = settings.get("loud_error") msg = 'standard-format error: %s' % err.decode('utf-8').strip() print(msg) if settings.get("log_errors"): print(err) sublime.error_message(msg) if loud else sublime.status_message(msg) class ToggleStandardFormatCommand(sublime_plugin.TextCommand): def run(self, edit): if settings.get('format_on_save', False): settings.set('format_on_save', False) sublime.status_message("Format on save: Off") else: settings.set('format_on_save', True) sublime.status_message("Format on save: On") sublime.save_settings(SETTINGS_FILE) def is_checked(self): return settings.get('format_on_save', False)