├── .python-version ├── .gitattributes ├── .gitignore ├── EditorConfig.sublime-settings ├── .editorconfig ├── Main.sublime-menu ├── editorconfig ├── __init__.py ├── compat.py ├── exceptions.py ├── versiontools.py ├── LICENSE.BSD ├── main.py ├── handler.py ├── ini.py └── fnmatch.py ├── EditorConfig.tmPreferences ├── editorconfig.sublime-snippet ├── license ├── readme.md ├── plugin.py └── EditorConfig.sublime-syntax /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *.cache 4 | -------------------------------------------------------------------------------- /EditorConfig.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [".editorconfig"], 3 | // Show debug logging 4 | "debug": false 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": [ 5 | { 6 | "id": "package-settings", 7 | "children": [ 8 | { 9 | "caption": "EditorConfig", 10 | "command": "edit_settings", 11 | "args": { 12 | "base_file": "${packages}/EditorConfig/EditorConfig.sublime-settings", 13 | "default": "{\n\t$0\n}\n" 14 | } 15 | } 16 | ] 17 | } 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /editorconfig/__init__.py: -------------------------------------------------------------------------------- 1 | """EditorConfig Python Core""" 2 | 3 | from .versiontools import join_version 4 | 5 | VERSION = (0, 12, 2, "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 .handler import EditorConfigHandler 19 | from .exceptions import * 20 | -------------------------------------------------------------------------------- /editorconfig/compat.py: -------------------------------------------------------------------------------- 1 | """EditorConfig Python2/Python3 compatibility utilities""" 2 | import sys 3 | 4 | 5 | __all__ = ['force_unicode', 'u'] 6 | 7 | 8 | if sys.version_info[0] == 2: 9 | text_type = unicode 10 | else: 11 | text_type = str 12 | 13 | 14 | def force_unicode(string): 15 | if not isinstance(string, text_type): 16 | string = text_type(string, encoding='utf-8') 17 | return string 18 | 19 | 20 | if sys.version_info[0] == 2: 21 | import codecs 22 | u = lambda s: codecs.unicode_escape_decode(s)[0] 23 | else: 24 | u = lambda s: s 25 | -------------------------------------------------------------------------------- /EditorConfig.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Comments 7 | scope 8 | source.ini.editorconfig 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | ; 18 | 19 | 20 | 21 | uuid 22 | aca72d19-945d-4f74-9e88-b7d80be2fb1b 23 | 24 | 25 | -------------------------------------------------------------------------------- /editorconfig/exceptions.py: -------------------------------------------------------------------------------- 1 | """EditorConfig exception classes 2 | 3 | Licensed under Simplified BSD License (see LICENSE.BSD 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.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 13 | editorconfig 14 | source.ini.editorconfig,text.plain 15 | 16 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /editorconfig/versiontools.py: -------------------------------------------------------------------------------- 1 | """EditorConfig version tools 2 | 3 | Provides ``join_version`` and ``split_version`` classes for converting 4 | __version__ strings to VERSION tuples and vice versa. 5 | 6 | """ 7 | 8 | import re 9 | 10 | 11 | __all__ = ['join_version', 'split_version'] 12 | 13 | 14 | _version_re = re.compile(r'^(\d+)\.(\d+)\.(\d+)(\..*)?$', re.VERBOSE) 15 | 16 | 17 | def join_version(version_tuple): 18 | """Return a string representation of version from given VERSION tuple""" 19 | version = "%s.%s.%s" % version_tuple[:3] 20 | if version_tuple[3] != "final": 21 | version += "-%s" % version_tuple[3] 22 | return version 23 | 24 | 25 | def split_version(version): 26 | """Return VERSION tuple for given string representation of version""" 27 | match = _version_re.search(version) 28 | if not match: 29 | return None 30 | else: 31 | split_version = list(match.groups()) 32 | if split_version[3] is None: 33 | split_version[3] = "final" 34 | split_version = list(map(int, split_version[:3])) + split_version[3:] 35 | return tuple(split_version) 36 | -------------------------------------------------------------------------------- /editorconfig/LICENSE.BSD: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2018 EditorConfig Team, including Hong Xu and Trey Hunner 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright notice, 9 | this list of conditions and the following disclaimer in the documentation 10 | and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 13 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 16 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 17 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 18 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 19 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 20 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 21 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 22 | POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # EditorConfig 2 | 3 | > [EditorConfig](https://editorconfig.org) helps developers maintain consistent coding styles between different editors 4 | 5 | 6 | ## Install 7 | 8 | Install `EditorConfig` with [Package Control](https://packagecontrol.io) and restart Sublime. 9 | 10 | 11 | ## Getting started 12 | 13 | See the [EditorConfig site][] for documentation. 14 | 15 | 16 | ## Supported properties 17 | 18 | - root 19 | - indent_style 20 | - indent_size 21 | - end\_of\_line 22 | - charset 23 | - trim_trailing_whitespace 24 | - insert_final_newline 25 | 26 | Explanation of the properties can be found on the [EditorConfig site][]. 27 | 28 | The `tab_width` property is intentionally not supported. 29 | 30 | 31 | ## Example file 32 | 33 | *My recommended default settings* 34 | 35 | ```ini 36 | root = true 37 | 38 | [*] 39 | indent_style = tab 40 | end_of_line = lf 41 | charset = utf-8 42 | trim_trailing_whitespace = true 43 | insert_final_newline = true 44 | ``` 45 | 46 | 47 | ## Tips 48 | 49 | ### EditorConfig snippet 50 | 51 | If you can't remember all settings managed by the EditorConfig file, you'll love the `editorconfig` snippet. 52 | 53 | Just type `editorconfig` + tab, and your editor will focus on the first setting's value (indent_style = *lf*). You can change the value, if you want, and jump to the next setting's value by hitting tab and so on. Settings are somewhat autocompleted, and if you don't remember all possible values, simply remove the setting value to see them all as a comment. 54 | 55 | You can be in a context where `editorconfig` + tab trigger another snippet. In that case, simply use `Goto anywhere` (Ctrl + P on Linux/Windows or + P on macOS), type `editorconfig`, select `Snippet: editorconfig` and hit Enter. 56 | 57 | ### View active config 58 | 59 | The active config is printed in the Sublime console. 60 | 61 | ### Trailing whitespace 62 | 63 | Even though there is a `trim_trailing_whitespace` property. I would still recommend you set `"draw_white_space": "all"` and/or `"trim_trailing_white_space_on_save": true` in your Sublime settings to prevent you from accidentally committing whitespace garbage whenever a project is missing a .editorconfig file. 64 | 65 | 66 | [EditorConfig site]: https://editorconfig.org 67 | -------------------------------------------------------------------------------- /editorconfig/main.py: -------------------------------------------------------------------------------- 1 | """EditorConfig command line interface 2 | 3 | Licensed under Simplified BSD License (see LICENSE.BSD file). 4 | 5 | """ 6 | 7 | import getopt 8 | import sys 9 | 10 | from . import VERSION, __version__ 11 | from .compat import force_unicode 12 | from .exceptions import ParsingError, PathError, VersionError 13 | from .handler import EditorConfigHandler 14 | from .versiontools import split_version 15 | 16 | 17 | def version(): 18 | print("EditorConfig Python Core Version %s" % __version__) 19 | 20 | 21 | def usage(command, error=False): 22 | if error: 23 | out = sys.stderr 24 | else: 25 | out = sys.stdout 26 | out.write("%s [OPTIONS] FILENAME\n" % command) 27 | out.write('-f ' 28 | 'Specify conf filename other than ".editorconfig".\n') 29 | out.write("-b " 30 | "Specify version (used by devs to test compatibility).\n") 31 | out.write("-h OR --help Print this help message.\n") 32 | out.write("-v OR --version Display version information.\n") 33 | 34 | 35 | def main(): 36 | command_name = sys.argv[0] 37 | try: 38 | opts, args = getopt.getopt(list(map(force_unicode, sys.argv[1:])), 39 | "vhb:f:", ["version", "help"]) 40 | except getopt.GetoptError as e: 41 | print(str(e)) 42 | usage(command_name, error=True) 43 | sys.exit(2) 44 | 45 | version_tuple = VERSION 46 | conf_filename = '.editorconfig' 47 | 48 | for option, arg in opts: 49 | if option in ('-h', '--help'): 50 | usage(command_name) 51 | sys.exit() 52 | if option in ('-v', '--version'): 53 | version() 54 | sys.exit() 55 | if option == '-f': 56 | conf_filename = arg 57 | if option == '-b': 58 | version_tuple = split_version(arg) 59 | if version_tuple is None: 60 | sys.exit("Invalid version number: %s" % arg) 61 | 62 | if len(args) < 1: 63 | usage(command_name, error=True) 64 | sys.exit(2) 65 | filenames = args 66 | multiple_files = len(args) > 1 67 | 68 | for filename in filenames: 69 | handler = EditorConfigHandler(filename, conf_filename, version_tuple) 70 | try: 71 | options = handler.get_configurations() 72 | except (ParsingError, PathError, VersionError) as e: 73 | print(str(e)) 74 | sys.exit(2) 75 | if multiple_files: 76 | print("[%s]" % filename) 77 | for key, value in options.items(): 78 | print("%s=%s" % (key, value)) 79 | -------------------------------------------------------------------------------- /plugin.py: -------------------------------------------------------------------------------- 1 | import pprint 2 | import sublime 3 | import sublime_plugin 4 | from .editorconfig import get_properties, EditorConfigError 5 | 6 | LINE_ENDINGS = { 7 | 'lf': 'unix', 8 | 'crlf': 'windows', 9 | 'cr': 'cr' 10 | } 11 | 12 | CHARSETS = { 13 | 'latin1': 'Western (ISO 8859-1)', 14 | 'utf-8': 'utf-8', 15 | 'utf-8-bom': 'utf-8 with bom', 16 | 'utf-16be': 'utf-16 be', 17 | 'utf-16le': 'utf-16 le' 18 | } 19 | 20 | def unexpanduser(path): 21 | from os.path import expanduser 22 | return path.replace(expanduser('~'), '~') 23 | 24 | def log(msg): 25 | print('EditorConfig: %s' % msg) 26 | 27 | def debug(msg): 28 | if sublime.load_settings('EditorConfig.sublime-settings').get('debug', False): 29 | log(msg) 30 | 31 | class EditorConfig(sublime_plugin.EventListener): 32 | MARKER = 'editorconfig' 33 | 34 | def on_load(self, view): 35 | if not view.settings().has(self.MARKER): 36 | self.init(view, 'load') 37 | 38 | def on_activated(self, view): 39 | if not view.settings().has(self.MARKER): 40 | self.init(view, 'activated') 41 | 42 | def on_pre_save(self, view): 43 | self.init(view, 'pre_save') 44 | 45 | def on_post_save(self, view): 46 | if not view.settings().has(self.MARKER): 47 | self.init(view, 'post_save') 48 | 49 | def init(self, view, event): 50 | path = view.file_name() 51 | if not path: 52 | return 53 | 54 | try: 55 | config = get_properties(path) 56 | except EditorConfigError: 57 | print('Error occurred while getting EditorConfig properties') 58 | else: 59 | if config: 60 | if event == 'activated' or event == 'load': 61 | debug('File Path \n%s' % unexpanduser(path)) 62 | debug('Applied Settings \n%s' % pprint.pformat(config)) 63 | if event == 'pre_save': 64 | self.apply_pre_save(view, config) 65 | else: 66 | self.apply_config(view, config) 67 | 68 | def apply_pre_save(self, view, config): 69 | settings = view.settings() 70 | spaces = settings.get('translate_tabs_to_spaces') 71 | charset = config.get('charset') 72 | end_of_line = config.get('end_of_line') 73 | indent_style = config.get('indent_style') 74 | insert_final_newline = config.get('insert_final_newline') 75 | if charset in CHARSETS: 76 | view.set_encoding(CHARSETS[charset]) 77 | if end_of_line in LINE_ENDINGS: 78 | view.set_line_endings(LINE_ENDINGS[end_of_line]) 79 | if indent_style == 'space' and spaces == False: 80 | view.run_command('expand_tabs', {'set_translate_tabs': True}) 81 | elif indent_style == 'tab' and spaces == True: 82 | view.run_command('unexpand_tabs', {'set_translate_tabs': True}) 83 | if insert_final_newline == 'false': 84 | view.run_command('remove_final_newlines') 85 | 86 | def apply_config(self, view, config): 87 | settings = view.settings() 88 | indent_style = config.get('indent_style') 89 | indent_size = config.get('indent_size') 90 | trim_trailing_whitespace = config.get('trim_trailing_whitespace') 91 | insert_final_newline = config.get('insert_final_newline') 92 | if indent_style == 'space': 93 | settings.set('translate_tabs_to_spaces', True) 94 | elif indent_style == 'tab': 95 | settings.set('translate_tabs_to_spaces', False) 96 | if indent_size: 97 | try: 98 | settings.set('tab_size', int(indent_size)) 99 | except ValueError: 100 | pass 101 | if trim_trailing_whitespace == 'true': 102 | settings.set('trim_trailing_white_space_on_save', True) 103 | elif trim_trailing_whitespace == 'false': 104 | settings.set('trim_trailing_white_space_on_save', False) 105 | if insert_final_newline == 'true': 106 | settings.set('ensure_newline_at_eof_on_save', True) 107 | elif insert_final_newline == 'false': 108 | settings.set('ensure_newline_at_eof_on_save', False) 109 | 110 | view.settings().set(self.MARKER, True) 111 | 112 | class RemoveFinalNewlinesCommand(sublime_plugin.TextCommand): 113 | def run(self, edit): 114 | region = self.view.find(r'\n*\Z', 0) 115 | self.view.erase(edit, region) 116 | -------------------------------------------------------------------------------- /EditorConfig.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # https://www.sublimetext.com/docs/syntax.html 4 | # https://editorconfig-specification.readthedocs.io/#file-format 5 | name: EditorConfig 6 | scope: source.ini.editorconfig 7 | 8 | file_extensions: 9 | - .editorconfig 10 | 11 | contexts: 12 | main: 13 | - include: comment 14 | - include: section 15 | - include: mapping 16 | 17 | comment: 18 | - match: ; 19 | scope: punctuation.definition.comment.ini 20 | push: 21 | - meta_scope: comment.line.semicolon.ini 22 | - match: \n 23 | pop: true 24 | - match: \# 25 | scope: punctuation.definition.comment.ini 26 | push: 27 | - meta_scope: comment.line.number-sign.ini 28 | - match: \n 29 | pop: true 30 | 31 | section: 32 | - match: \[ 33 | scope: meta.section.ini punctuation.definition.section.begin.ini 34 | push: 35 | - meta_content_scope: meta.section.ini entity.name.section.ini 36 | - match: \] 37 | scope: meta.section.ini punctuation.definition.section.end.ini 38 | pop: true 39 | - match: \\\S 40 | scope: constant.character.escape.ini 41 | - match: / 42 | scope: punctuation.separator.slash.ini 43 | - include: glob-expression 44 | - include: eol-pop 45 | 46 | # https://editorconfig-specification.readthedocs.io/#glob-expressions 47 | glob-expression: 48 | - match: '[*?]' 49 | scope: constant.character.wildcard.ini 50 | - match: (\[)(!)?\w+(\]) 51 | scope: meta.set.ini 52 | captures: 53 | 1: punctuation.section.brackets.begin.ini 54 | 2: keyword.operator.logical.ini 55 | 3: punctuation.section.brackets.end.ini 56 | - match: \{ 57 | scope: punctuation.section.braces.begin.ini 58 | push: 59 | - meta_scope: meta.set.ini 60 | - match: \} 61 | scope: punctuation.section.braces.end.ini 62 | pop: true 63 | - match: \, 64 | scope: punctuation.separator.sequence.ini 65 | - match: \.\. 66 | scope: punctuation.separator.range.ini 67 | - include: eol-pop 68 | 69 | # https://editorconfig-specification.readthedocs.io/#supported-pairs 70 | mapping: 71 | - match: (?=\S) 72 | push: 73 | - meta_content_scope: meta.mapping.key.ini 74 | - match: (?=\s*=) 75 | set: 76 | - - meta_content_scope: meta.mapping.ini 77 | - match: (?=\S) 78 | set: 79 | - mapping-value-meta 80 | - mapping-value 81 | - include: eol-pop 82 | - - match: = 83 | scope: punctuation.separator.key-value.ini 84 | pop: true 85 | - match: \b(?:indent_style|indent_size|tab_width|end_of_line|charset|trim_trailing_whitespace|insert_final_newline|root)\b 86 | scope: variable.language.ini 87 | - include: string 88 | - include: eol-pop 89 | 90 | mapping-value: 91 | - match: \b(?i:tab|space|lf|cr(?:lf)?|latin1|utf-8(?:-bom)?|utf-16[bl]e)\b 92 | scope: support.constant.ini 93 | - match: \b(?i:true)\b 94 | scope: constant.language.boolean.true.ini 95 | - match: \b(?i:false)\b 96 | scope: constant.language.boolean.false.ini 97 | - include: number 98 | - include: string 99 | - include: eol-pop 100 | 101 | mapping-value-meta: 102 | - meta_scope: meta.mapping.value.ini 103 | - match: '' 104 | pop: true 105 | 106 | number: 107 | - match: '([-+])?\b(\d*(\.)\d+(?:(?:E|e)[-+]?\d+)?)(F|f)?\b' 108 | scope: meta.number.float.decimal.ini 109 | captures: 110 | 1: keyword.operator.arithmetic.ini 111 | 2: constant.numeric.value.ini 112 | 3: punctuation.separator.decimal.ini 113 | 4: constant.numeric.suffix.ini 114 | - match: '([-+])?\b(\d+)\b' 115 | scope: meta.number.integer.decimal.ini 116 | captures: 117 | 1: keyword.operator.arithmetic.ini 118 | 2: constant.numeric.value.ini 119 | 120 | string: 121 | - match: \" 122 | scope: punctuation.definition.string.begin.ini 123 | push: 124 | - meta_scope: string.quoted.double.ini 125 | - include: character-escape 126 | - match: \" 127 | scope: punctuation.definition.string.end.ini 128 | pop: true 129 | - match: \n 130 | pop: true 131 | - match: \' 132 | scope: punctuation.definition.string.begin.ini 133 | push: 134 | - meta_scope: string.quoted.single.ini 135 | - include: character-escape 136 | - match: \' 137 | scope: punctuation.definition.string.end.ini 138 | pop: true 139 | - match: \n 140 | pop: true 141 | - match: (?=\S) 142 | push: 143 | - meta_content_scope: string.unquoted.ini 144 | - include: character-escape 145 | - match: (?=[\s=:,\[]) 146 | pop: true 147 | 148 | character-escape: 149 | - match: \\(?:[^*\s\w]|[abnrt0]|x\h{4}) 150 | scope: constant.character.escape.ini 151 | 152 | eol-pop: 153 | - match: $|(?=\s+[;#]) 154 | pop: true 155 | -------------------------------------------------------------------------------- /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 Simplified BSD License (see LICENSE.BSD file). 7 | 8 | """ 9 | 10 | import os 11 | 12 | from . import VERSION 13 | from .exceptions import PathError, VersionError 14 | from .ini import EditorConfigParser 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 | def find_up(directory, start_path): 32 | """ 33 | Return true if ``start_path`` is child of the ``directory``, 34 | false otherwise 35 | 36 | ``start_path`` is the full path to a file or folder 37 | ``directory`` is the name of the directory to find in the path 38 | 39 | e.g. find_up("foo", "/root/foo/bar") -> true 40 | find_up("hom", "/home/foo/bar")-> false 41 | """ 42 | new_path, base_name = os.path.split(start_path) 43 | old_path=start_path 44 | # Stop the while when the root is reached 45 | while old_path != new_path: 46 | if base_name == directory: 47 | return True 48 | old_path = new_path 49 | new_path, base_name = os.path.split(old_path) 50 | return False 51 | 52 | class EditorConfigHandler(object): 53 | 54 | """ 55 | Allows locating and parsing of EditorConfig files for given filename 56 | 57 | In addition to the constructor a single public method is provided, 58 | ``get_configurations`` which returns the EditorConfig options for 59 | the ``filepath`` specified to the constructor. 60 | 61 | """ 62 | 63 | def __init__(self, filepath, conf_filename='.editorconfig', 64 | version=VERSION): 65 | """Create EditorConfigHandler for matching given filepath""" 66 | self.filepath = filepath 67 | self.conf_filename = conf_filename 68 | self.version = version 69 | self.options = None 70 | 71 | def get_configurations(self): 72 | 73 | """ 74 | Find EditorConfig files and return all options matching filepath 75 | 76 | Special exceptions that may be raised by this function include: 77 | 78 | - ``VersionError``: self.version is invalid EditorConfig version 79 | - ``PathError``: self.filepath is not a valid absolute filepath 80 | - ``ParsingError``: improperly formatted EditorConfig file found 81 | 82 | """ 83 | 84 | self.check_assertions() 85 | path, filename = os.path.split(self.filepath) 86 | conf_files = get_filenames(path, self.conf_filename) 87 | 88 | # Attempt to find and parse every EditorConfig file in filetree 89 | for filename in conf_files: 90 | parser = EditorConfigParser(self.filepath) 91 | parser.read(filename) 92 | 93 | # Merge new EditorConfig file's options into current options 94 | old_options = self.options 95 | self.options = parser.options 96 | if old_options: 97 | self.options.update(old_options) 98 | 99 | # Stop parsing if parsed file has a ``root = true`` option 100 | if parser.root_file: 101 | break 102 | 103 | self.preprocess_values() 104 | return self.options 105 | 106 | def check_assertions(self): 107 | 108 | """Raise error if filepath or version have invalid values""" 109 | 110 | # Raise ``PathError`` if filepath isn't an absolute path 111 | if not os.path.isabs(self.filepath): 112 | raise PathError("Input file must be a full path name.") 113 | 114 | # Raise ``VersionError`` if version specified is greater than current 115 | if self.version is not None and self.version[:3] > VERSION[:3]: 116 | raise VersionError( 117 | "Required version is greater than the current version.") 118 | 119 | def preprocess_values(self): 120 | 121 | """Preprocess option values for consumption by plugins""" 122 | 123 | opts = self.options 124 | 125 | # Lowercase option value for certain options 126 | for name in ["end_of_line", "indent_style", "indent_size", 127 | "insert_final_newline", "trim_trailing_whitespace", 128 | "charset"]: 129 | if name in opts: 130 | opts[name] = opts[name].lower() 131 | 132 | # Set indent_size to "tab" if indent_size is unspecified and 133 | # indent_style is set to "tab". 134 | if (opts.get("indent_style") == "tab" and 135 | not "indent_size" in opts and self.version >= (0, 10, 0)): 136 | opts["indent_size"] = "tab" 137 | 138 | # Set tab_width to indent_size if indent_size is specified and 139 | # tab_width is unspecified 140 | if ("indent_size" in opts and "tab_width" not in opts and 141 | opts["indent_size"] != "tab"): 142 | opts["tab_width"] = opts["indent_size"] 143 | 144 | # Set indent_size to tab_width if indent_size is "tab" 145 | if ("indent_size" in opts and "tab_width" in opts and 146 | opts["indent_size"] == "tab"): 147 | opts["indent_size"] = opts["tab_width"] 148 | 149 | # Set end_of_line to lf if in a “.git” directory 150 | if find_up(".git", self.filepath): 151 | opts["end_of_line"] = "lf" 152 | -------------------------------------------------------------------------------- /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.PSF 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 posixpath 17 | import re 18 | from codecs import open 19 | from collections import OrderedDict 20 | from os import sep 21 | from os.path import dirname, normpath 22 | 23 | from .compat import u 24 | from .exceptions import ParsingError 25 | from .fnmatch import fnmatch 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""" 42 | 43 | \s * # Optional whitespace 44 | \[ # Opening square brace 45 | 46 | (?P
# One or more characters excluding 47 | ( [^\#;] | \\\# | \\; ) + # unescaped # and ; characters 48 | ) 49 | 50 | \] # Closing square brace 51 | 52 | """, re.VERBOSE 53 | ) 54 | # Regular expression for parsing option name/values. 55 | # Allow any amount of whitespaces, followed by separator 56 | # (either ``:`` or ``=``), followed by any amount of whitespace and then 57 | # any characters to eol 58 | OPTCRE = re.compile( 59 | r""" 60 | 61 | \s * # Optional whitespace 62 | (?P