diff --git a/README.md b/README.md index ae106b2..08b7c1d 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,17 @@ Use to `"format_on_save": true`. - To change settings on a per-package basis, add them under `ClangFormat` key, example project.sublime-settings: +- To whitelist/blacklist formatting of files and folders for format on save, the +`file_exclude_patterns`,`folder_exclude_patterns`,`file_include_patterns`,`folder_include_patterns` keys may be used; these patterns work similar to the project [file patterns](https://www.sublimetext.com/docs/file_patterns.html) +- The include/exclude patterns do not affect a manual format command, manual format commands will always format a file or selection ```json { "folders": [], "settings": { "ClangFormat": { - "format_on_save": true + "format_on_save": true, + "file_exclude_patterns":["//my_folder/*.generated.h"], } } } diff --git a/clang_format.py b/clang_format.py index d1539de..152e51b 100644 --- a/clang_format.py +++ b/clang_format.py @@ -1,5 +1,6 @@ import sublime, sublime_plugin import subprocess, os +import re # The styles available by default. We add one option: "Custom". This tells @@ -188,6 +189,11 @@ def load_settings(): global style global format_on_save global languages + global file_include_patterns + global folder_include_patterns + global file_exclude_patterns + global folder_exclude_patterns + settings_global = sublime.load_settings(settings_file) settings_local = sublime.active_window().active_view().settings().get('ClangFormat', {}) load = lambda name, default: settings_local.get(name, settings_global.get(name, default)) @@ -196,6 +202,10 @@ def load_settings(): style = load('style', styles[0]) format_on_save = load('format_on_save', False) languages = load('languages', ['C', 'C++', 'C++11', 'JavaScript']) + file_include_patterns = load('file_include_patterns', []) + folder_include_patterns = load('folder_include_patterns', []) + file_exclude_patterns = load('file_exclude_patterns', []) + folder_exclude_patterns = load('folder_exclude_patterns', []) def is_supported(lang): @@ -203,6 +213,97 @@ def is_supported(lang): return any((lang.endswith((l + '.tmLanguage', l + '.sublime-syntax')) for l in languages)) +def get_project_root(view: sublime.View): + window = view.window() + if not window or not window.is_valid(): + return None + + project_file = window.project_file_name() + + if not project_file: + return None + + return os.path.dirname(project_file) + +def match(pattern: str, value: str, project_root: str, is_folder: bool): + # Windows is case insensitive so convert all to lower case + if os_is_windows: + pattern = pattern.lower() + value = value.lower().replace('\\', '/') # normalize windows path to use forward slash '/' + project_root = project_root.lower().replace('\\', '/') if project_root is not None else None + + if pattern.startswith('//'): + if project_root is None or not value.startswith(project_root): + return False + pattern = pattern[2:] + value = value[len(project_root) + 1:] # strip the starting slash as well + + # Convert pattern into regex + if not is_folder: + # files always have a start and end + pattern = '^' + pattern + pattern = pattern + '$' + + pattern = pattern.replace('.','\\.') # escape any dots + pattern = pattern.replace('?','[^/]') # replace '?' with "any single character except '/'" + pattern = pattern.replace('*/','\n\n\n') # set temp marker, we're assuming no one's going to put in 3x newlines in the pattern + pattern = pattern.replace('/*','\r\r\r') # set temp marker, we're assuming no one's going to put in 3x carriage returns in the pattern + pattern = pattern.replace('*','[^/]*') # singular asterisk have non-greedy lookups + pattern = pattern.replace('\n\n\n','.*/') # replace temp marker, convert '*/' into greedy lookups + pattern = pattern.replace('\r\r\r','/.*') # replace temp marker, convert '/*' into greedy lookups + + return re.search(pattern, value) is not None + +def should_execute(view: sublime.View): + file = view.file_name() + if file is None: + return False + + folder = os.path.dirname(file) + project_root = get_project_root(view) + folder_include_empty = len(folder_include_patterns) == 0 + folder_exclude_empty = len(folder_exclude_patterns) == 0 + file_include_empty = len(file_include_patterns) == 0 + file_exclude_empty = len(file_exclude_patterns) == 0 + + folder_include = any(match(p, folder, project_root, True) for p in folder_include_patterns) + folder_exclude = any(match(p, folder, project_root, True) for p in folder_exclude_patterns) + file_include = any(match(p, file, project_root, False) for p in file_include_patterns) + file_exclude = any(match(p, file, project_root, False) for p in file_exclude_patterns) + + ''' + If all lists are not empty the following precedence is used: + `file_exclude > file_include > folder_exclude > folder_include` + + If a list is empty, remove it from the precedence list, e.g. if file_include_list is empty: + `file_exclude > folder_exclude > folder_include` + In other words, a file will be formatted if it has no match in the file and folder exclude lists and + has a match in the folder include list + + If all lists empty the file will be formatted + + ''' + result = True + if not file_exclude_empty: + if file_exclude: + return False + + if not file_include_empty: + if file_include: + return True + result = False + + if not folder_exclude_empty: + if folder_exclude: + return False + + if not folder_include_empty: + if folder_include: + return True + result = False + + return result + # Triggered when the user runs clang format. class ClangFormatCommand(sublime_plugin.TextCommand): def run(self, edit, whole_buffer=False): @@ -309,7 +410,7 @@ def on_pre_save(self, view): if is_supported(syntax): # Ensure that settings are up to date. load_settings() - if format_on_save: + if format_on_save and should_execute(view): print("Auto-applying Clang Format on save.") view.run_command("clang_format", {"whole_buffer": True}) diff --git a/clang_format.sublime-settings b/clang_format.sublime-settings index cbfe217..6c413d0 100644 --- a/clang_format.sublime-settings +++ b/clang_format.sublime-settings @@ -27,5 +27,10 @@ // has its syntax set to a language in the list below. If it is in the list, // then the file will be formatted by ClangFormat. - "languages": ["C", "C++", "C++11", "JavaScript", "Objective-C", "Objective-C++"] + "languages": ["C", "C++", "C++11", "JavaScript", "Objective-C", "Objective-C++"], + + "file_include_patterns": [], + "folder_include_patterns": [], + "file_exclude_patterns": [], + "folder_exclude_patterns": [] } diff --git a/clang_format_custom.sublime-settings b/clang_format_custom.sublime-settings index dad9674..e4b8aff 100644 --- a/clang_format_custom.sublime-settings +++ b/clang_format_custom.sublime-settings @@ -1,6 +1,6 @@ { // All these settings have been taken from the clang-format manual, - // and can be customised form within Sublime Text settings files. + // and can be customised from within Sublime Text settings files. // Please note, the defaults set below are completely random values. // Take a look at http://clang.llvm.org/docs/ClangFormatStyleOptions.html // For examples.