├── .editorconfig ├── .gitattributes ├── EditorConfig.py ├── editorconfig ├── __init__.py ├── compat.py ├── exceptions.py ├── fnmatch.py ├── handler.py ├── ini.py ├── main.py ├── odict.py └── versiontools.py └── readme.md /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /EditorConfig.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | from editorconfig import get_properties, EditorConfigError 3 | 4 | 5 | LINE_ENDINGS = { 6 | 'lf': 'unix', 7 | 'crlf': 'windows', 8 | 'cr': 'cr' 9 | } 10 | 11 | CHARSETS = { 12 | 'latin1': 'Western (ISO 8859-1)', 13 | 'utf-8': 'utf-8', 14 | 'utf-8-bom': 'utf-8 with bom', 15 | 'utf-16be': 'utf-16 be', 16 | 'utf-16le': 'utf-16 le' 17 | } 18 | 19 | class EditorConfig(sublime_plugin.EventListener): 20 | def on_load(self, view): 21 | self.init(view, False) 22 | 23 | def on_pre_save(self, view): 24 | self.init(view, True) 25 | 26 | def init(self, view, pre_save): 27 | path = view.file_name() 28 | if not path: 29 | return 30 | try: 31 | config = get_properties(path) 32 | except EditorConfigError: 33 | print 'Error occurred while getting EditorConfig properties' 34 | else: 35 | if config: 36 | if pre_save: 37 | self.apply_charset(view, config) 38 | else: 39 | self.apply_config(view, config) 40 | 41 | def apply_charset(self, view, config): 42 | charset = config.get('charset') 43 | if charset in CHARSETS: 44 | view.set_encoding(CHARSETS[charset]) 45 | 46 | def apply_config(self, view, config): 47 | settings = view.settings() 48 | indent_style = config.get('indent_style') 49 | indent_size = config.get('indent_size') 50 | end_of_line = config.get('end_of_line') 51 | trim_trailing_whitespace = config.get('trim_trailing_whitespace') 52 | insert_final_newline = config.get('insert_final_newline') 53 | if indent_style == 'space': 54 | settings.set('translate_tabs_to_spaces', True) 55 | elif indent_style == 'tab': 56 | settings.set('translate_tabs_to_spaces', False) 57 | if indent_size: 58 | try: 59 | settings.set('tab_size', int(indent_size)) 60 | except ValueError: 61 | pass 62 | if end_of_line in LINE_ENDINGS: 63 | view.set_line_endings(LINE_ENDINGS[end_of_line]) 64 | if trim_trailing_whitespace == 'true': 65 | settings.set('trim_trailing_white_space_on_save', True) 66 | elif trim_trailing_whitespace == 'false': 67 | settings.set('trim_trailing_white_space_on_save', False) 68 | if insert_final_newline == 'true': 69 | settings.set('ensure_newline_at_eof_on_save', True) 70 | elif insert_final_newline == 'false': 71 | settings.set('ensure_newline_at_eof_on_save', False) 72 | -------------------------------------------------------------------------------- /editorconfig/__init__.py: -------------------------------------------------------------------------------- 1 | """EditorConfig Python Core""" 2 | 3 | from editorconfig.versiontools import join_version 4 | 5 | VERSION = (0, 11, 1, "final") 6 | 7 | __all__ = ['get_properties', 'EditorConfigError', 'exceptions'] 8 | 9 | __version__ = join_version(VERSION) 10 | 11 | 12 | def get_properties(filename): 13 | """Locate and parse EditorConfig files for the given filename""" 14 | handler = EditorConfigHandler(filename) 15 | return handler.get_configurations() 16 | 17 | 18 | from editorconfig.handler import EditorConfigHandler 19 | from editorconfig.exceptions import * 20 | -------------------------------------------------------------------------------- /editorconfig/compat.py: -------------------------------------------------------------------------------- 1 | """EditorConfig Python2/Python3/Jython compatibility utilities""" 2 | import sys 3 | import types 4 | 5 | __all__ = ['slice', 'u'] 6 | 7 | 8 | if sys.version_info[0] == 2: 9 | slice = types.SliceType 10 | else: 11 | slice = slice 12 | 13 | 14 | if sys.version_info[0] == 2: 15 | import codecs 16 | u = lambda s: codecs.unicode_escape_decode(s)[0] 17 | else: 18 | u = lambda s: s 19 | -------------------------------------------------------------------------------- /editorconfig/exceptions.py: -------------------------------------------------------------------------------- 1 | """EditorConfig exception classes 2 | 3 | Licensed under PSF License (see LICENSE.txt file). 4 | 5 | """ 6 | 7 | 8 | class EditorConfigError(Exception): 9 | """Parent class of all exceptions raised by EditorConfig""" 10 | 11 | 12 | try: 13 | from ConfigParser import ParsingError as _ParsingError 14 | except: 15 | from configparser import ParsingError as _ParsingError 16 | 17 | 18 | class ParsingError(_ParsingError, EditorConfigError): 19 | """Error raised if an EditorConfig file could not be parsed""" 20 | 21 | 22 | class PathError(ValueError, EditorConfigError): 23 | """Error raised if invalid filepath is specified""" 24 | 25 | 26 | class VersionError(ValueError, EditorConfigError): 27 | """Error raised if invalid version number is specified""" 28 | -------------------------------------------------------------------------------- /editorconfig/fnmatch.py: -------------------------------------------------------------------------------- 1 | """Filename matching with shell patterns. 2 | 3 | fnmatch(FILENAME, PATTERN) matches according to the local convention. 4 | fnmatchcase(FILENAME, PATTERN) always takes case in account. 5 | 6 | The functions operate by translating the pattern into a regular 7 | expression. They cache the compiled regular expressions for speed. 8 | 9 | The function translate(PATTERN) returns a regular expression 10 | corresponding to PATTERN. (It does not compile it.) 11 | 12 | Based on code from fnmatch.py file distributed with Python 2.6. 13 | 14 | Licensed under PSF License (see LICENSE.txt file). 15 | 16 | Changes to original fnmatch module: 17 | - translate function supports ``*`` and ``**`` similarly to fnmatch C library 18 | """ 19 | 20 | import os 21 | import re 22 | 23 | __all__ = ["fnmatch", "fnmatchcase", "translate"] 24 | 25 | _cache = {} 26 | 27 | 28 | def fnmatch(name, pat): 29 | """Test whether FILENAME matches PATTERN. 30 | 31 | Patterns are Unix shell style: 32 | 33 | - ``*`` matches everything except path separator 34 | - ``**`` matches everything 35 | - ``?`` matches any single character 36 | - ``[seq]`` matches any character in seq 37 | - ``[!seq]`` matches any char not in seq 38 | - ``{s1,s2,s3}`` matches any of the strings given (separated by commas) 39 | 40 | An initial period in FILENAME is not special. 41 | Both FILENAME and PATTERN are first case-normalized 42 | if the operating system requires it. 43 | If you don't want this, use fnmatchcase(FILENAME, PATTERN). 44 | """ 45 | 46 | name = os.path.normcase(name).replace(os.sep, "/") 47 | return fnmatchcase(name, pat) 48 | 49 | 50 | def fnmatchcase(name, pat): 51 | """Test whether FILENAME matches PATTERN, including case. 52 | 53 | This is a version of fnmatch() which doesn't case-normalize 54 | its arguments. 55 | """ 56 | 57 | if not pat in _cache: 58 | res = translate(pat) 59 | _cache[pat] = re.compile(res) 60 | return _cache[pat].match(name) is not None 61 | 62 | 63 | def translate(pat): 64 | """Translate a shell PATTERN to a regular expression. 65 | 66 | There is no way to quote meta-characters. 67 | """ 68 | 69 | i, n = 0, len(pat) 70 | res = '' 71 | escaped = False 72 | while i < n: 73 | c = pat[i] 74 | i = i + 1 75 | if c == '*': 76 | j = i 77 | if j < n and pat[j] == '*': 78 | res = res + '.*' 79 | else: 80 | res = res + '[^/]*' 81 | elif c == '?': 82 | res = res + '.' 83 | elif c == '[': 84 | j = i 85 | if j < n and pat[j] == '!': 86 | j = j + 1 87 | if j < n and pat[j] == ']': 88 | j = j + 1 89 | while j < n and (pat[j] != ']' or escaped): 90 | escaped = pat[j] == '\\' and not escaped 91 | j = j + 1 92 | if j >= n: 93 | res = res + '\\[' 94 | else: 95 | stuff = pat[i:j] 96 | i = j + 1 97 | if stuff[0] == '!': 98 | stuff = '^' + stuff[1:] 99 | elif stuff[0] == '^': 100 | stuff = '\\' + stuff 101 | res = '%s[%s]' % (res, stuff) 102 | elif c == '{': 103 | j = i 104 | groups = [] 105 | while j < n and pat[j] != '}': 106 | k = j 107 | while k < n and (pat[k] not in (',', '}') or escaped): 108 | escaped = pat[k] == '\\' and not escaped 109 | k = k + 1 110 | group = pat[j:k] 111 | for char in (',', '}', '\\'): 112 | group = group.replace('\\' + char, char) 113 | groups.append(group) 114 | j = k 115 | if j < n and pat[j] == ',': 116 | j = j + 1 117 | if j < n and pat[j] == '}': 118 | groups.append('') 119 | if j >= n or len(groups) < 2: 120 | res = res + '\\{' 121 | else: 122 | res = '%s(%s)' % (res, '|'.join(map(re.escape, groups))) 123 | i = j + 1 124 | else: 125 | res = res + re.escape(c) 126 | return res + '\Z(?ms)' 127 | -------------------------------------------------------------------------------- /editorconfig/handler.py: -------------------------------------------------------------------------------- 1 | """EditorConfig file handler 2 | 3 | Provides ``EditorConfigHandler`` class for locating and parsing 4 | EditorConfig files relevant to a given filepath. 5 | 6 | Licensed under PSF License (see LICENSE.txt file). 7 | 8 | """ 9 | 10 | import os 11 | 12 | from editorconfig import VERSION 13 | from editorconfig.ini import EditorConfigParser 14 | from editorconfig.exceptions import PathError, VersionError 15 | 16 | 17 | __all__ = ['EditorConfigHandler'] 18 | 19 | 20 | def get_filenames(path, filename): 21 | """Yield full filepath for filename in each directory in and above path""" 22 | path_list = [] 23 | while True: 24 | path_list.append(os.path.join(path, filename)) 25 | newpath = os.path.dirname(path) 26 | if path == newpath: 27 | break 28 | path = newpath 29 | return path_list 30 | 31 | 32 | class EditorConfigHandler(object): 33 | 34 | """ 35 | Allows locating and parsing of EditorConfig files for given filename 36 | 37 | In addition to the constructor a single public method is provided, 38 | ``get_configurations`` which returns the EditorConfig options for 39 | the ``filepath`` specified to the constructor. 40 | 41 | """ 42 | 43 | def __init__(self, filepath, conf_filename='.editorconfig', version=None): 44 | """Create EditorConfigHandler for matching given filepath""" 45 | self.filepath = filepath 46 | self.conf_filename = conf_filename 47 | self.version = version 48 | self.options = None 49 | 50 | def get_configurations(self): 51 | 52 | """ 53 | Find EditorConfig files and return all options matching filepath 54 | 55 | Special exceptions that may be raised by this function include: 56 | 57 | - ``VersionError``: self.version is invalid EditorConfig version 58 | - ``PathError``: self.filepath is not a valid absolute filepath 59 | - ``ParsingError``: improperly formatted EditorConfig file found 60 | 61 | """ 62 | 63 | self.check_assertions() 64 | path, filename = os.path.split(self.filepath) 65 | conf_files = get_filenames(path, self.conf_filename) 66 | 67 | # Attempt to find and parse every EditorConfig file in filetree 68 | for filename in conf_files: 69 | parser = EditorConfigParser(self.filepath) 70 | parser.read(filename) 71 | 72 | # Merge new EditorConfig file's options into current options 73 | old_options = self.options 74 | self.options = parser.options 75 | if old_options: 76 | self.options.update(old_options) 77 | 78 | # Stop parsing if parsed file has a ``root = true`` option 79 | if parser.root_file: 80 | break 81 | 82 | self.preprocess_values() 83 | return self.options 84 | 85 | def check_assertions(self): 86 | 87 | """Raise error if filepath or version have invalid values""" 88 | 89 | # Raise ``PathError`` if filepath isn't an absolute path 90 | if not os.path.isabs(self.filepath): 91 | raise PathError("Input file must be a full path name.") 92 | 93 | # Raise ``VersionError`` if version specified is greater than current 94 | if self.version is not None and self.version[:3] > VERSION[:3]: 95 | raise VersionError( 96 | "Required version is greater than the current version.") 97 | 98 | def preprocess_values(self): 99 | 100 | """Preprocess option values for consumption by plugins""" 101 | 102 | opts = self.options 103 | 104 | # Lowercase option value for certain options 105 | for name in ["end_of_line", "indent_style", "indent_size", 106 | "insert_final_newline", "trim_trailing_whitespace", "charset"]: 107 | if name in opts: 108 | opts[name] = opts[name].lower() 109 | 110 | # Set indent_size to "tab" if indent_size is unspecified and 111 | # indent_style is set to "tab". 112 | if (opts.get("indent_style") == "tab" and 113 | not "indent_size" in opts and self.version >= VERSION[:3]): 114 | opts["indent_size"] = "tab" 115 | 116 | # Set tab_width to indent_size if indent_size is specified and 117 | # tab_width is unspecified 118 | if ("indent_size" in opts and "tab_width" not in opts and 119 | opts["indent_size"] != "tab"): 120 | opts["tab_width"] = opts["indent_size"] 121 | 122 | # Set indent_size to tab_width if indent_size is "tab" 123 | if ("indent_size" in opts and "tab_width" in opts and 124 | opts["indent_size"] == "tab"): 125 | opts["indent_size"] = opts["tab_width"] 126 | -------------------------------------------------------------------------------- /editorconfig/ini.py: -------------------------------------------------------------------------------- 1 | """EditorConfig file parser 2 | 3 | Based on code from ConfigParser.py file distributed with Python 2.6. 4 | 5 | Licensed under PSF License (see LICENSE.txt file). 6 | 7 | Changes to original ConfigParser: 8 | 9 | - Special characters can be used in section names 10 | - Octothorpe can be used for comments (not just at beginning of line) 11 | - Only track INI options in sections that match target filename 12 | - Stop parsing files with when ``root = true`` is found 13 | 14 | """ 15 | 16 | import re 17 | from codecs import open 18 | import posixpath 19 | from os import sep 20 | from os.path import normcase, dirname 21 | 22 | from editorconfig.exceptions import ParsingError 23 | from editorconfig.fnmatch import fnmatch 24 | from editorconfig.odict import OrderedDict 25 | from editorconfig.compat import u 26 | 27 | 28 | __all__ = ["ParsingError", "EditorConfigParser"] 29 | 30 | 31 | class EditorConfigParser(object): 32 | 33 | """Parser for EditorConfig-style configuration files 34 | 35 | Based on RawConfigParser from ConfigParser.py in Python 2.6. 36 | """ 37 | 38 | # Regular expressions for parsing section headers and options. 39 | # Allow ``]`` and escaped ``;`` and ``#`` characters in section headers 40 | SECTCRE = re.compile( 41 | r'\s*\[' # [ 42 | r'(?P
([^#;]|\\#|\\;)+)' # very permissive! 43 | r'\]' # ] 44 | ) 45 | # Regular expression for parsing option name/values. 46 | # Allow any amount of whitespaces, followed by separator 47 | # (either ``:`` or ``=``), followed by any amount of whitespace and then 48 | # any characters to eol 49 | OPTCRE = re.compile( 50 | r'\s*(?P