├── .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