├── .python-version ├── run_tests.sh ├── tests ├── __init__.py ├── test_json.py └── validate_json_format.py ├── bh_modules ├── __init__.py ├── lowercase.py ├── pyquotes.py ├── phpkeywords.py ├── cmakekeywords.py ├── erlangcase.py ├── pascalkeywords.py ├── s840d_gcode.py ├── luakeywords.py ├── s840d_hmi.py ├── mdcode.py ├── foldbracket.py ├── bashsupport.py ├── tagnameselect.py ├── latexenvironments.py ├── swapbrackets.py ├── swapquotes.py ├── bracketremove.py ├── rubykeywords.py ├── tagattrselect.py ├── bracketselect.py └── tags.py ├── icons ├── icon_version.json ├── dot.png ├── tag.png ├── cross.png ├── hash.png ├── minus.png ├── plus.png ├── quote.png ├── star.png ├── bookmark.png ├── circle.png ├── question.png ├── dot_small.png ├── hash_small.png ├── plus_small.png ├── star_small.png ├── tag_small.png ├── angle_bracket.png ├── circle_small.png ├── cross_small.png ├── curly_bracket.png ├── double_quote.png ├── minus_small.png ├── quote_small.png ├── round_bracket.png ├── single_quote.png ├── bookmark_small.png ├── question_small.png ├── square_bracket.png ├── angle_bracket_open.png ├── curly_bracket_open.png ├── double_quote_close.png ├── double_quote_open.png ├── double_quote_small.png ├── round_bracket_open.png ├── single_quote_close.png ├── single_quote_open.png ├── single_quote_small.png ├── angle_bracket_close.png ├── angle_bracket_small.png ├── curly_bracket_close.png ├── curly_bracket_small.png ├── double_quote_offset.png ├── round_bracket_close.png ├── round_bracket_small.png ├── single_quote_offset.png ├── square_bracket_close.png ├── square_bracket_open.png ├── square_bracket_small.png ├── double_quote_open_small.png ├── single_quote_open_small.png ├── angle_bracket_close_small.png ├── angle_bracket_open_small.png ├── curly_bracket_close_small.png ├── curly_bracket_open_small.png ├── double_quote_close_small.png ├── double_quote_offset_open.png ├── double_quote_offset_small.png ├── round_bracket_close_small.png ├── round_bracket_open_small.png ├── single_quote_close_small.png ├── single_quote_offset_open.png ├── single_quote_offset_small.png ├── square_bracket_open_small.png ├── square_bracket_close_small.png ├── double_quote_offset_open_small.png └── single_quote_offset_open_small.png ├── .gitignore ├── docs ├── src │ ├── markdown │ │ ├── .snippets │ │ │ ├── refs.md │ │ │ ├── abbr.md │ │ │ └── links.md │ │ ├── images │ │ │ ├── popup1.png │ │ │ ├── popup2.png │ │ │ ├── Example1.png │ │ │ └── unmatched_popup.png │ │ ├── about │ │ │ ├── license.md │ │ │ └── contributing.md │ │ ├── index.md │ │ ├── extended-regex.md │ │ ├── usage.md │ │ └── installation.md │ ├── requirements.txt │ └── dictionary │ │ └── en-custom.txt └── theme │ └── announce.html ├── .github ├── FUNDING.yml ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── deploy.yml │ └── build.yml ├── ISSUE_TEMPLATE.md └── labels.yml ├── messages.json ├── .compat_flag_3067 ├── dependencies.json ├── messages ├── recent.md └── install.md ├── .prospector.yml ├── Default.sublime-keymap ├── bh_logging.py ├── tox.ini ├── bh_remove.py ├── readme.md ├── bh_swapping.py ├── bh_swapping.sublime-settings ├── .pyspelling.yml ├── mkdocs.yml ├── quickstart.md ├── bh_tag.sublime-settings ├── bh_wrapping.sublime-settings ├── Example.sublime-keymap ├── bh_plugin.py ├── support.py ├── Default.sublime-commands ├── CHANGES.md ├── bh_popup.py └── bh_wrapping.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | py.test . 2 | flake8 . 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit Tests.""" 2 | -------------------------------------------------------------------------------- /bh_modules/__init__.py: -------------------------------------------------------------------------------- 1 | """BH Modules.""" 2 | -------------------------------------------------------------------------------- /icons/icon_version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | site/* 4 | build/* 5 | .cache/* 6 | .tox/* 7 | -------------------------------------------------------------------------------- /docs/src/markdown/.snippets/refs.md: -------------------------------------------------------------------------------- 1 | --8<-- 2 | links.md 3 | abbr.md 4 | --8<-- 5 | -------------------------------------------------------------------------------- /icons/dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/dot.png -------------------------------------------------------------------------------- /icons/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/tag.png -------------------------------------------------------------------------------- /icons/cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/cross.png -------------------------------------------------------------------------------- /icons/hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/hash.png -------------------------------------------------------------------------------- /icons/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/minus.png -------------------------------------------------------------------------------- /icons/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/plus.png -------------------------------------------------------------------------------- /icons/quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/quote.png -------------------------------------------------------------------------------- /icons/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/star.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: facelessuser 2 | custom: 3 | - "https://www.paypal.me/facelessuser" 4 | -------------------------------------------------------------------------------- /icons/bookmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/bookmark.png -------------------------------------------------------------------------------- /icons/circle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/circle.png -------------------------------------------------------------------------------- /icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/question.png -------------------------------------------------------------------------------- /icons/dot_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/dot_small.png -------------------------------------------------------------------------------- /icons/hash_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/hash_small.png -------------------------------------------------------------------------------- /icons/plus_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/plus_small.png -------------------------------------------------------------------------------- /icons/star_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/star_small.png -------------------------------------------------------------------------------- /icons/tag_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/tag_small.png -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": "messages/install.md", 3 | "2.33.0": "messages/recent.md" 4 | } 5 | -------------------------------------------------------------------------------- /icons/angle_bracket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket.png -------------------------------------------------------------------------------- /icons/circle_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/circle_small.png -------------------------------------------------------------------------------- /icons/cross_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/cross_small.png -------------------------------------------------------------------------------- /icons/curly_bracket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket.png -------------------------------------------------------------------------------- /icons/double_quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote.png -------------------------------------------------------------------------------- /icons/minus_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/minus_small.png -------------------------------------------------------------------------------- /icons/quote_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/quote_small.png -------------------------------------------------------------------------------- /icons/round_bracket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket.png -------------------------------------------------------------------------------- /icons/single_quote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote.png -------------------------------------------------------------------------------- /icons/bookmark_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/bookmark_small.png -------------------------------------------------------------------------------- /icons/question_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/question_small.png -------------------------------------------------------------------------------- /icons/square_bracket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket.png -------------------------------------------------------------------------------- /docs/src/markdown/.snippets/abbr.md: -------------------------------------------------------------------------------- 1 | *[BH]: BracketHighlighter 2 | *[ST2]: Sublime Text 2 3 | *[ST3]: Sublime Text 3 4 | -------------------------------------------------------------------------------- /icons/angle_bracket_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket_open.png -------------------------------------------------------------------------------- /icons/curly_bracket_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket_open.png -------------------------------------------------------------------------------- /icons/double_quote_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_close.png -------------------------------------------------------------------------------- /icons/double_quote_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_open.png -------------------------------------------------------------------------------- /icons/double_quote_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_small.png -------------------------------------------------------------------------------- /icons/round_bracket_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket_open.png -------------------------------------------------------------------------------- /icons/single_quote_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_close.png -------------------------------------------------------------------------------- /icons/single_quote_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_open.png -------------------------------------------------------------------------------- /icons/single_quote_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_small.png -------------------------------------------------------------------------------- /icons/angle_bracket_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket_close.png -------------------------------------------------------------------------------- /icons/angle_bracket_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket_small.png -------------------------------------------------------------------------------- /icons/curly_bracket_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket_close.png -------------------------------------------------------------------------------- /icons/curly_bracket_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket_small.png -------------------------------------------------------------------------------- /icons/double_quote_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_offset.png -------------------------------------------------------------------------------- /icons/round_bracket_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket_close.png -------------------------------------------------------------------------------- /icons/round_bracket_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket_small.png -------------------------------------------------------------------------------- /icons/single_quote_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_offset.png -------------------------------------------------------------------------------- /icons/square_bracket_close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket_close.png -------------------------------------------------------------------------------- /icons/square_bracket_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket_open.png -------------------------------------------------------------------------------- /icons/square_bracket_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket_small.png -------------------------------------------------------------------------------- /icons/double_quote_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_open_small.png -------------------------------------------------------------------------------- /icons/single_quote_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_open_small.png -------------------------------------------------------------------------------- /docs/src/markdown/images/popup1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/docs/src/markdown/images/popup1.png -------------------------------------------------------------------------------- /docs/src/markdown/images/popup2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/docs/src/markdown/images/popup2.png -------------------------------------------------------------------------------- /icons/angle_bracket_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket_close_small.png -------------------------------------------------------------------------------- /icons/angle_bracket_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/angle_bracket_open_small.png -------------------------------------------------------------------------------- /icons/curly_bracket_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket_close_small.png -------------------------------------------------------------------------------- /icons/curly_bracket_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/curly_bracket_open_small.png -------------------------------------------------------------------------------- /icons/double_quote_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_close_small.png -------------------------------------------------------------------------------- /icons/double_quote_offset_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_offset_open.png -------------------------------------------------------------------------------- /icons/double_quote_offset_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_offset_small.png -------------------------------------------------------------------------------- /icons/round_bracket_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket_close_small.png -------------------------------------------------------------------------------- /icons/round_bracket_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/round_bracket_open_small.png -------------------------------------------------------------------------------- /icons/single_quote_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_close_small.png -------------------------------------------------------------------------------- /icons/single_quote_offset_open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_offset_open.png -------------------------------------------------------------------------------- /icons/single_quote_offset_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_offset_small.png -------------------------------------------------------------------------------- /icons/square_bracket_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket_open_small.png -------------------------------------------------------------------------------- /docs/src/markdown/images/Example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/docs/src/markdown/images/Example1.png -------------------------------------------------------------------------------- /icons/square_bracket_close_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/square_bracket_close_small.png -------------------------------------------------------------------------------- /icons/double_quote_offset_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/double_quote_offset_open_small.png -------------------------------------------------------------------------------- /icons/single_quote_offset_open_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/icons/single_quote_offset_open_small.png -------------------------------------------------------------------------------- /docs/src/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs_pymdownx_material_extras>=2.0 2 | mkdocs-git-revision-date-localized-plugin 3 | mkdocs-minify-plugin 4 | pyspelling 5 | -------------------------------------------------------------------------------- /docs/src/markdown/images/unmatched_popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/facelessuser/BracketHighlighter/HEAD/docs/src/markdown/images/unmatched_popup.png -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please follow this link to see Contributing & Support documentation: http://facelessuser.github.io/BracketHighlighter/contributing/. 2 | -------------------------------------------------------------------------------- /.compat_flag_3067: -------------------------------------------------------------------------------- 1 | Temporary file so that Jon of Sublime Text can avoid loading older BH on ST versions >= 3067. There was a nasty hang and crash when Jon updated ST3 to version 3067. 2 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | ">=3000": [ 4 | "backrefs" 5 | ], 6 | ">=3124": [ 7 | "backrefs", 8 | "mdpopups" 9 | ] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /bh_modules/lowercase.py: -------------------------------------------------------------------------------- 1 | """Ensure brackets are lower case.""" 2 | 3 | 4 | def validate(name, bracket, bracket_side, bfr): 5 | """Check if bracket is lowercase.""" 6 | 7 | return bfr[bracket.begin:bracket.end].islower() 8 | -------------------------------------------------------------------------------- /docs/theme/announce.html: -------------------------------------------------------------------------------- 1 | Sponsorship 2 | is now available! 3 | 4 | {% set icon = "octicons/heart-fill-16" %} 5 | {% include ".icons/" ~ icon ~ ".svg" %} 6 | 7 | -------------------------------------------------------------------------------- /messages/recent.md: -------------------------------------------------------------------------------- 1 | # BracketHighlighter 2 | 3 | New release! 4 | 5 | A restart might be required. If you see issues immediately after the update 6 | please try restarting. 7 | 8 | ## 2.33.0 9 | 10 | - **NEW**: Release special branch for Sublime Text 4201+. 11 | -------------------------------------------------------------------------------- /messages/install.md: -------------------------------------------------------------------------------- 1 | # BracketHighlighter 2 | 3 | Welcome to BracketHighlighter! 4 | 5 | A restart of Sublime Text is recommended to ensure all dependencies get 6 | loaded properly. 7 | 8 | For a quick start guide, please go to 9 | `Preferences->Package Settings->BracketHighlighter->Quick Start Guide`. 10 | -------------------------------------------------------------------------------- /.prospector.yml: -------------------------------------------------------------------------------- 1 | strictness: medium 2 | doc-warnings: true 3 | test-warnings: false 4 | max-line-length: 120 5 | ignore-patterns: 6 | - docs 7 | - docs_theme 8 | - site(\\|/).*\.py 9 | dodgy: 10 | run: false 11 | pep257: 12 | disable: 13 | - D202 14 | - D203 15 | - D401 16 | pylint: 17 | disable: 18 | - import-error 19 | -------------------------------------------------------------------------------- /bh_modules/pyquotes.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | 8 | 9 | def compare(name, first, second, bfr): 10 | """Pair the appropriate open bracket with its close.""" 11 | 12 | return bfr[first.begin:first.end] == bfr[second.begin:second.end] 13 | -------------------------------------------------------------------------------- /bh_modules/phpkeywords.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | 8 | 9 | def compare(name, first, second, bfr): 10 | """Pair the appropriate open bracket with its close.""" 11 | 12 | return "end" + bfr[first.begin:first.end].lower() == bfr[second.begin:second.end].lower() 13 | -------------------------------------------------------------------------------- /bh_modules/cmakekeywords.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | 8 | 9 | def compare(name, first, second, bfr): 10 | """Pair the appropriate open bracket with its close.""" 11 | 12 | return "end" + bfr[first.begin:first.end].lower() == bfr[second.begin:second.end].lower() 13 | -------------------------------------------------------------------------------- /bh_modules/erlangcase.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter.bh_plugin import import_module 8 | lowercase = import_module("bh_modules.lowercase") 9 | 10 | 11 | def validate(*args): 12 | """Check if bracket is lowercase.""" 13 | return lowercase.validate(*args) 14 | -------------------------------------------------------------------------------- /bh_modules/pascalkeywords.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | 8 | 9 | def compare(name, first, second, bfr): 10 | """Differentiate 'repeat..until' from '*..end' brackets.""" 11 | brackets = (bfr[first.begin:first.end], bfr[second.begin:second.end]) 12 | return (brackets[0] == "repeat") ^ (brackets[1] == "end") 13 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // Navigate tabstops in wrapped selection 3 | { 4 | "keys": ["tab"], 5 | "command": "bh_next_wrap_sel", 6 | "context": 7 | [ 8 | { 9 | "operand": true, 10 | "operator": "equal", 11 | "match_all": true, 12 | "key": "bh_wrapping" 13 | } 14 | ] 15 | } 16 | ] 17 | -------------------------------------------------------------------------------- /bh_logging.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | 9 | 10 | def log(msg): 11 | """Standard log.""" 12 | 13 | print("BracketHighlighter: %s" % msg) 14 | 15 | 16 | def debug(msg): 17 | """Debug log.""" 18 | 19 | if sublime.load_settings("bh_core.sublime-settings").get('debug_enable', False): 20 | log(msg) 21 | -------------------------------------------------------------------------------- /bh_modules/s840d_gcode.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Deathaxe 5 | License: MIT 6 | """ 7 | 8 | 9 | def compare(name, first, second, bfr): 10 | """Ensure correct open is paired with correct close.""" 11 | 12 | o = bfr[first.begin:first.end].lower() 13 | c = bfr[second.begin:second.end].lower() 14 | 15 | match = False 16 | if o == "repeat" and c == "until": 17 | match = True 18 | elif c == "end" + o: 19 | match = True 20 | return match 21 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist = 4 | py37,p38,py39,py310,p311,lint 5 | 6 | [testenv] 7 | deps= 8 | pytest 9 | commands= 10 | py.test . 11 | 12 | [testenv:documents] 13 | deps= 14 | -rdocs/src/requirements.txt 15 | commands= 16 | mkdocs build --clean --verbose --strict 17 | pyspelling 18 | 19 | [testenv:lint] 20 | deps= 21 | flake8 22 | flake8_docstrings 23 | pep8-naming 24 | flake8-mutable 25 | flake8-builtins 26 | commands= 27 | flake8 "{toxinidir}" 28 | 29 | [flake8] 30 | ignore=D202,D203,D401,W504,W606,E741,N818 31 | max-line-length=120 32 | exclude=site/*.py,.tox/* 33 | -------------------------------------------------------------------------------- /bh_modules/luakeywords.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter.bh_plugin import import_module 8 | lowercase = import_module("bh_modules.lowercase") 9 | 10 | 11 | def validate(*args): 12 | """Check if bracket is lowercase.""" 13 | return lowercase.validate(*args) 14 | 15 | 16 | def compare(name, first, second, bfr): 17 | """Differentiate 'repeat..until' from '*..end' brackets.""" 18 | brackets = (bfr[first.begin:first.end], bfr[second.begin:second.end]) 19 | return (brackets[0] == "repeat") ^ (brackets[1] == "end") 20 | -------------------------------------------------------------------------------- /bh_modules/s840d_hmi.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Deathaxe 5 | License: MIT 6 | """ 7 | 8 | S840D_HMI_CLASSES = ("//a", "//b", "//g", "//m", "//s") 9 | 10 | 11 | def compare(name, first, second, bfr): 12 | """Ensure correct open is paired with correct close.""" 13 | 14 | o = bfr[first.begin:first.end].lower() 15 | c = bfr[second.begin:second.end].lower() 16 | 17 | match = False 18 | # classes 19 | if o in S840D_HMI_CLASSES and c == "//end": 20 | match = True 21 | # methods 22 | elif c == "end_" + o or c == 'end' + o: 23 | match = True 24 | return match 25 | -------------------------------------------------------------------------------- /bh_modules/mdcode.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | 8 | 9 | def post_match(view, name, style, first, second, center, bfr, threshold): 10 | """Ensure that backticks that do not contribute inside the inline or block regions are not highlighted.""" 11 | 12 | if first is not None and second is not None: 13 | diff = first.size() - second.size() 14 | if diff > 0: 15 | first = first.move(first.begin, first.end - diff) 16 | elif diff < 0: 17 | second = second.move(second.begin - diff, second.end) 18 | return first, second, style 19 | -------------------------------------------------------------------------------- /docs/src/markdown/.snippets/links.md: -------------------------------------------------------------------------------- 1 | [aprosopo]: https://github.com/facelessuser/Aprosopo 2 | [backrefs]: http://facelessuser.github.io/backrefs/ 3 | [keymap]: https://github.com/facelessuser/BracketHighlighter/blob/master/Example.sublime-keymap 4 | [mkdocs]: http://www.mkdocs.org 5 | [mkdocs-material]: https://github.com/squidfunk/mkdocs-material 6 | [regreplace]: https://github.com/facelessuser/RegReplace 7 | [package-control]: https://packagecontrol.io/ 8 | [package-control-install]: https://packagecontrol.io/installation 9 | [pymdown-extensions]: https://github.com/facelessuser/pymdown-extensions 10 | [scopes]: http://facelessuser.github.io/sublime-markdown-popups/textmate_scopes/ 11 | [template]: https://github.com/facelessuser/BracketHighlighter/blob/master/.github/ISSUE_TEMPLATE.md 12 | -------------------------------------------------------------------------------- /bh_modules/foldbracket.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter import bh_plugin 8 | import sublime 9 | 10 | 11 | class FoldBrackets(bh_plugin.BracketPluginCommand): 12 | """Fold bracket plugin.""" 13 | 14 | def run(self, edit, name): 15 | """Fold the content between the bracket.""" 16 | 17 | content = sublime.Region(self.left.end, self.right.begin) 18 | new_content = [content] 19 | if content.size() > 0: 20 | if self.view.fold(content) is False: 21 | new_content = self.view.unfold(content) 22 | self.selection = new_content 23 | 24 | 25 | def plugin(): 26 | """Make plugin available.""" 27 | 28 | return FoldBrackets 29 | -------------------------------------------------------------------------------- /bh_modules/bashsupport.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter.bh_plugin import import_module 8 | lowercase = import_module("bh_modules.lowercase") 9 | 10 | BASH_KEYWORDS = ("select", "for", "while", "until") 11 | 12 | 13 | def validate(*args): 14 | """Check if bracket is lowercase.""" 15 | return lowercase.validate(*args) 16 | 17 | 18 | def compare(name, first, second, bfr): 19 | """Ensure correct open is paired with correct close.""" 20 | 21 | o = bfr[first.begin:first.end] 22 | c = bfr[second.begin:second.end] 23 | 24 | match = False 25 | if o == "if" and c == "fi": 26 | match = True 27 | elif o in BASH_KEYWORDS and c == "done": 28 | match = True 29 | elif o == "case" and c == "esac": 30 | match = True 31 | return match 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks you for contributing to this project! Make sure you've read: http://facelessuser.github.io/BracketHighlighter/contributing/. Please follow the guidelines below. 2 | 3 | - Please describe the change in as much detail as possible so I can understand what is being added or modified. 4 | 5 | - If you are solving a bug that does not already have an issue, please describe the bug in detail and provide info on how to reproduce if applicable (this is good for me and others to reference later when verifying the issue has been resolved). 6 | 7 | - Please reference and link related open bugs or feature requests in this pull if applicable. 8 | 9 | - Make sure you've documented or updated the existing documentation if introducing a new feature or modifying the behavior of an existing feature that a user needs to be aware of. I will not accept new features if you have not provided documentation describing the feature. 10 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'st3-*' 7 | - 'st4-*' 8 | 9 | jobs: 10 | 11 | documents: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up Python 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: 3.11 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip setuptools 26 | python -m pip install -r docs/src/requirements.txt 27 | - name: Deploy documents 28 | run: | 29 | git config user.name facelessuser 30 | git config user.email "${{ secrets.GH_EMAIL }}" 31 | git remote add gh-token "https://${{ secrets.GH_TOKEN }}@github.com/facelessuser/BracketHighlighter.git" 32 | git fetch gh-token && git fetch gh-token gh-pages:gh-pages 33 | python -m mkdocs gh-deploy -v --clean --remote-name gh-token 34 | git push gh-token gh-pages 35 | -------------------------------------------------------------------------------- /bh_modules/tagnameselect.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | from BracketHighlighter import bh_plugin 9 | from BracketHighlighter.bh_plugin import import_module 10 | tags = import_module("bh_modules.tags") 11 | 12 | 13 | class TagNameSelect(bh_plugin.BracketPluginCommand): 14 | """Tag name select plugin.""" 15 | 16 | def run(self, edit, name): 17 | """Select tag name.""" 18 | 19 | if self.left.size() > 1: 20 | tag_settings = sublime.load_settings("bh_tag.sublime-settings") 21 | tag_mode = tags.get_tag_mode(self.view, tag_settings.get("tag_mode", [])) 22 | tag_name = tag_settings.get('tag_name')[tag_mode] 23 | region1 = self.view.find(tag_name, self.left.begin) 24 | region2 = self.view.find(tag_name, self.right.begin) 25 | self.selection = [region1, region2] 26 | 27 | 28 | def plugin(): 29 | """Make plugin available.""" 30 | 31 | return TagNameSelect 32 | -------------------------------------------------------------------------------- /bh_modules/latexenvironments.py: -------------------------------------------------------------------------------- 1 | """Compare environments to ensure they have matching names.""" 2 | import re 3 | 4 | BEGIN_RE = re.compile(r"\\begin\{([^\}]*)\}") 5 | END_RE = re.compile(r"\\end\{([^\}]*)\}") 6 | 7 | BEGIN_LEN = len("\\begin{") 8 | END_LEN = len("\\end{") 9 | BRACKET_LEN = len("}") 10 | 11 | 12 | def highlighting(view, name, style, left, right): 13 | """Highlight only the environment name.""" 14 | if left is not None: 15 | left = left.move(left.begin + BEGIN_LEN, left.end - BRACKET_LEN) 16 | if right is not None: 17 | right = right.move(right.begin + END_LEN, right.end - BRACKET_LEN) 18 | 19 | return left, right 20 | 21 | 22 | def compare(name, first, second, bfr): 23 | """Ensure both environments have the same name.""" 24 | first_match = BEGIN_RE.match(bfr[first.begin:first.end]) 25 | second_match = END_RE.match(bfr[second.begin:second.end]) 26 | # although it should never happen, avoid errors from not matched regex 27 | if not (first_match and second_match): 28 | return False 29 | 30 | return first_match.group(1) == second_match.group(1) 31 | -------------------------------------------------------------------------------- /bh_modules/swapbrackets.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | from BracketHighlighter.bh_plugin import import_module 9 | BracketRemove = import_module("bh_modules.bracketremove", "BracketRemove") 10 | 11 | 12 | class SwapBrackets(BracketRemove): 13 | """Swap bracket plugin.""" 14 | 15 | def run(self, edit, name, remove_content=False, remove_indent=False, remove_block=False): 16 | """Remove then replace the bracket and adjust indentation if desired.""" 17 | 18 | offset = self.left.toregion().size() 19 | selection = [sublime.Region(self.left.begin, self.right.begin - offset)] 20 | left = self.left.move(self.left.end, self.left.end) 21 | right = self.right.move(self.right.begin, self.right.begin) 22 | super(SwapBrackets, self).run(edit, name) 23 | self.selection = selection 24 | self.left = left 25 | self.right = right 26 | self.nobracket = False 27 | 28 | 29 | def plugin(): 30 | """Make plugin available.""" 31 | 32 | return SwapBrackets 33 | -------------------------------------------------------------------------------- /docs/src/markdown/about/license.md: -------------------------------------------------------------------------------- 1 | # License 2 | 3 | Released under the MIT license. 4 | 5 | Copyright (c) 2013 - 2025 Isaac Muse 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 9 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 10 | persons to whom the Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 13 | Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 16 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 17 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 18 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /tests/test_json.py: -------------------------------------------------------------------------------- 1 | """Test JSON.""" 2 | import unittest 3 | from . import validate_json_format 4 | import os 5 | import fnmatch 6 | 7 | 8 | class TestSettings(unittest.TestCase): 9 | """Test JSON settings.""" 10 | 11 | def _get_json_files(self, pattern, folder='.'): 12 | """Get JSON files.""" 13 | 14 | for root, dirnames, filenames in os.walk(folder): 15 | for filename in fnmatch.filter(filenames, pattern): 16 | yield os.path.join(root, filename) 17 | dirnames = [d for d in dirnames if d not in ('.svn', '.git', '.tox')] 18 | 19 | def test_json_settings(self): 20 | """Test each JSON file.""" 21 | 22 | patterns = ( 23 | '*.sublime-settings', 24 | '*.sublime-keymap', 25 | '*.sublime-commands', 26 | '*.sublime-menu', 27 | '*.sublime-theme', 28 | '*.sublime-color-scheme' 29 | ) 30 | 31 | for pattern in patterns: 32 | for f in self._get_json_files(pattern): 33 | self.assertFalse( 34 | validate_json_format.CheckJsonFormat(False, True).check_format(f), 35 | "%s does not comform to expected format!" % f 36 | ) 37 | -------------------------------------------------------------------------------- /docs/src/dictionary/en-custom.txt: -------------------------------------------------------------------------------- 1 | API 2 | Aprosopo 3 | BH 4 | Backrefs 5 | BoundInCode 6 | BracketHighligher 7 | BracketHighlighter 8 | CMFL 9 | CMake 10 | CSS 11 | CSSedit 12 | Changelog 13 | ColdFusion 14 | Control's 15 | Customizable 16 | Deathaxe 17 | Django 18 | EmojiOne 19 | Erlang 20 | GitHub 21 | GitLab 22 | JSON 23 | JavaScript 24 | Jun 25 | LaTeX 26 | Lua 27 | MERCHANTABILITY 28 | Mar 29 | MdPopups 30 | MkDocs 31 | NONINFRINGEMENT 32 | OCaml 33 | OSX 34 | Offscreen 35 | PHP 36 | PLIST 37 | PyMdown 38 | RegReplace 39 | Regex 40 | Sublime's 41 | SublimeBrackets 42 | SublimeTagmatcher 43 | Swappable 44 | TextMate 45 | Twemoji 46 | allowlist 47 | backticks 48 | blocklist 49 | bool 50 | boolean 51 | builtins 52 | changelog 53 | cmd 54 | ctrl 55 | customizable 56 | dev 57 | docstrings 58 | ebeweber 59 | emoji 60 | extendable 61 | facelessuser 62 | gforcada 63 | gitlab 64 | installable 65 | ish 66 | matcher 67 | minimap 68 | mkdocs 69 | multi 70 | multiline 71 | offscreen 72 | plugin 73 | plugins 74 | popup 75 | popup's 76 | popups 77 | pre 78 | preprocessors 79 | pycqa 80 | pymdown 81 | pyparadigm 82 | pyparadigm's 83 | pytest 84 | regex 85 | renderer 86 | requesters 87 | screenshot 88 | squidfunk 89 | sublicense 90 | tuple 91 | un 92 | unescaping 93 | unneeded 94 | versa 95 | whitelist 96 | whitespace 97 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | tags: 8 | - '**' 9 | pull_request: 10 | branches: 11 | - '**' 12 | 13 | jobs: 14 | tests: 15 | 16 | env: 17 | TOXENV: py311 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python 24 | uses: actions/setup-python@v4 25 | with: 26 | python-version: 3.11 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip setuptools tox 30 | - name: Tests 31 | run: | 32 | python -m tox 33 | 34 | lint: 35 | 36 | env: 37 | TOXENV: lint 38 | 39 | runs-on: ubuntu-latest 40 | 41 | steps: 42 | - uses: actions/checkout@v4 43 | - name: Set up Python 44 | uses: actions/setup-python@v4 45 | with: 46 | python-version: 3.11 47 | - name: Install dependencies 48 | run: | 49 | python -m pip install --upgrade pip setuptools tox 50 | - name: Lint 51 | run: | 52 | python -m tox 53 | 54 | documents: 55 | env: 56 | TOXENV: documents 57 | 58 | runs-on: ubuntu-latest 59 | 60 | steps: 61 | - uses: actions/checkout@v4 62 | - name: Set up Python 63 | uses: actions/setup-python@v4 64 | with: 65 | python-version: 3.11 66 | - name: Install dependencies 67 | run: | 68 | python -m pip install --upgrade pip setuptools tox 69 | - name: Install Aspell 70 | run: | 71 | sudo apt-get install aspell aspell-en 72 | - name: Build documents 73 | run: | 74 | python -m tox 75 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please read and fill out this template by replacing the instructions with appropriate information. If the template is not followed, the issue will be marked `Invalid` and closed. 2 | 3 | Before submitting an issue search past issues and read the area of the [documentation](http://facelessuser.github.io/BracketHighlighter/) related to your specific question, issue, or request. 4 | 5 | --- 6 | 7 | ## Description 8 | 9 | ... what is the issue / request ? 10 | 11 | > Vague issues/requests will be marked with `Insufficient Details` for about a week. If not corrected, they will be marked `Stale` for about a week and then closed. 12 | 13 | > For feature requests or proposals: 14 | 15 | > - Clearly define in as much detail as possible how you imagine the feature to work. 16 | > - Examples are also appreciated. 17 | 18 | > For bugs and support questions: 19 | 20 | > - Describe the bug/question in as much detail as possible to make it clear what is wrong or what you do not > understand. 21 | > - Provide errors from console (if available). 22 | > - Pictures or screencasts can also be used to clarify what the issue is or what the question is. 23 | > - Provide links to 3rd party syntax highlighting package you are using if applicable. 24 | 25 | ## Support Info 26 | 27 | ... 28 | 29 | > Run the following command from the menu: `Preferences->Package Settings->BracketHighlighter->Support Info`. Post the result here. 30 | 31 | ## Steps to Reproduce Issue 32 | 33 | 1. First step... 34 | 2. Second step... 35 | 3. Third step... 36 | 37 | > Provide steps to reproduce the issue. Pictures are fine, but also provide code/text I can copy and paste in order to reproduce. Omit for feature requests and feature proposals. 38 | -------------------------------------------------------------------------------- /bh_remove.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime_plugin 8 | from collections import namedtuple 9 | 10 | MENU = namedtuple("Menu", "simple content block block_indent")( 11 | "Remove Brackets", 12 | "Remove Brackets and Content", 13 | "Remove Brackets: Block", 14 | "Remove Brackets: Indented Block" 15 | ) 16 | 17 | 18 | class BhRemoveBracketsCommand(sublime_plugin.TextCommand): 19 | """Command to remove current highlighted brackets and optionally content.""" 20 | 21 | def remove_brackets(self, value): 22 | """Perform removal of brackets.""" 23 | 24 | if value != -1: 25 | menu_item = MENU[value] 26 | indent = menu_item == MENU.block_indent 27 | block = menu_item == MENU.block or menu_item == MENU.block_indent 28 | content = menu_item == MENU.content 29 | 30 | self.view.run_command( 31 | "bh_key", 32 | { 33 | "plugin": { 34 | "type": ["__all__"], 35 | "command": "bh_modules.bracketremove", 36 | "args": { 37 | "remove_indent": indent, 38 | "remove_block": block, 39 | "remove_content": content 40 | } 41 | } 42 | } 43 | ) 44 | 45 | def run(self, edit): 46 | """Show menu of removal options.""" 47 | 48 | self.window = self.view.window() 49 | self.window.show_quick_panel( 50 | list(MENU), 51 | self.remove_brackets 52 | ) 53 | -------------------------------------------------------------------------------- /docs/src/markdown/index.md: -------------------------------------------------------------------------------- 1 | # About BracketHighlighter 2 | 3 | ## Overview 4 | 5 | BracketHighlighter matches a variety of brackets such as: `[]`, `()`, `{}`, `""`, `''`, `#!xml `, and even 6 | custom brackets. 7 | 8 | This was originally forked from pyparadigm's _SublimeBrackets_ and _SublimeTagmatcher_ (both are no longer available). I 9 | forked his repositories to fix a number issues I add some features I had wanted. I also wanted to improve the 10 | efficiency of the matching. Moving forward, I have thrown away all of the code and have completely rewritten the entire 11 | code base to allow for more flexibility, faster matching, and a more feature rich experience. 12 | 13 | ![screenshot](images/Example1.png) 14 | 15 | ## Feature List 16 | 17 | - Customizable to highlight almost any bracket. 18 | - Customizable bracket highlight style. 19 | - High visibility bracket highlight mode. 20 | - Selectively disable or enable specific matching of tags, brackets, or quotes. 21 | - Selectively use an allowlist or blocklist for matching specific tags, brackets, or quotes based on language. 22 | - When bound to a shortcut, allow options to show line count and char count between match in the status bar. 23 | - Highlight basic brackets within strings. 24 | - Works with multi-select. 25 | - Configurable custom gutter icons. 26 | - Toggle bracket escape mode for string brackets (regex|string). 27 | - Bracket plugins that can jump between bracket ends, select content, etc. 28 | 29 | ## Credits 30 | 31 | - pyparadigm: for his original efforts with SublimeBrackets and SublimeTagmatcher which originally BracketHighlighter 32 | was built off of and the inspiration for this project. 33 | - BoundInCode: for his Tag icon. 34 | -------------------------------------------------------------------------------- /bh_modules/swapquotes.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter import bh_plugin 8 | import sublime 9 | 10 | 11 | class SwapQuotes(bh_plugin.BracketPluginCommand): 12 | """Swap quotes plugin.""" 13 | 14 | def escaped(self, idx): 15 | """Check if character is an escape char.""" 16 | 17 | view = self.view 18 | escaped = False 19 | while idx >= 0 and view.substr(idx) == '\\': 20 | escaped = ~escaped 21 | idx -= 1 22 | return escaped 23 | 24 | def run(self, edit, name): 25 | """ 26 | Swap double or single quotes with each other. 27 | 28 | Handle escaping or unescaping like quotes or 29 | unlike quotes respectively. 30 | """ 31 | 32 | view = self.view 33 | quote = view.substr(self.left.begin) 34 | if quote != "'" and quote != '"': 35 | return 36 | new = "'" if (quote == '"') else '"' 37 | old = quote 38 | begin = self.left.end 39 | end = self.right.begin 40 | content_end = self.right.begin 41 | 42 | view.replace(edit, self.left.toregion(), view.substr(self.left.toregion()).replace(old, new)) 43 | view.replace(edit, self.right.toregion(), view.substr(self.right.toregion()).replace(old, new)) 44 | 45 | offset = 0 46 | while begin < end + offset: 47 | char = view.substr(begin) 48 | if char == old and self.escaped(begin - 1): 49 | view.replace(edit, sublime.Region(begin - 1, begin), '') 50 | offset -= 1 51 | content_end -= 1 52 | elif char == new and not self.escaped(begin - 1): 53 | view.insert(edit, begin, "\\") 54 | offset += 1 55 | content_end += 1 56 | begin += 1 57 | 58 | self.right = self.right.move(content_end, end + offset) 59 | self.selection = [sublime.Region(content_end)] 60 | 61 | 62 | def plugin(): 63 | """Make plugin available.""" 64 | 65 | return SwapQuotes 66 | -------------------------------------------------------------------------------- /bh_modules/bracketremove.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter import bh_plugin 8 | import re 9 | import sublime 10 | 11 | 12 | class BracketRemove(bh_plugin.BracketPluginCommand): 13 | """Bracket remove plugin.""" 14 | 15 | def decrease_indent_level(self, edit, row_first, row_last): 16 | """Decrease indent level on removal.""" 17 | 18 | tab_size = self.view.settings().get("tab_size", 4) 19 | indents = re.compile(r"^(?:\t| {%d}| *)((?:\t| {%d}| )*)([\s\S]*)" % (tab_size, tab_size)) 20 | if not self.single_line: 21 | for x in reversed(range(row_first, row_last + 1)): 22 | line = self.view.full_line(self.view.text_point(x, 0)) 23 | text = self.view.substr(line) 24 | m = indents.match(text) 25 | if m: 26 | self.view.replace(edit, line, m.group(1) + m.group(2)) 27 | 28 | def run(self, edit, name, remove_content=False, remove_indent=False, remove_block=False): 29 | """Remove the given bracket and adjust its indentation if desired.""" 30 | 31 | if remove_content: 32 | self.view.replace(edit, sublime.Region(self.left.begin, self.right.end), "") 33 | else: 34 | row_first = self.view.rowcol(self.left.end)[0] + 1 35 | row_last = self.view.rowcol(self.right.begin)[0] - 1 36 | self.single_line = not row_first <= row_last 37 | if remove_block and not self.single_line: 38 | self.view.replace(edit, self.view.full_line(self.right.toregion()), "") 39 | else: 40 | self.view.replace(edit, self.right.toregion(), "") 41 | if remove_indent: 42 | self.decrease_indent_level(edit, row_first, row_last) 43 | if remove_block and not self.single_line: 44 | self.view.replace(edit, self.view.full_line(self.left.toregion()), "") 45 | else: 46 | self.view.replace(edit, self.left.toregion(), "") 47 | 48 | self.left = None 49 | self.right = None 50 | self.nobracket = True 51 | 52 | 53 | def plugin(): 54 | """Make plugin available.""" 55 | 56 | return BracketRemove 57 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Donate via PayPal][donate-image]][donate-link] 2 | [![Build][github-ci-image]][github-ci-link] 3 | [![Package Control Downloads][pc-image]][pc-link] 4 | ![License][license-image] 5 | # BracketHighlighter 6 | 7 | Bracket Highlighter matches a variety of brackets such as: `[]`, `()`, `{}`, `""`, `''`, ``, and even custom 8 | brackets. 9 | 10 | This was originally forked from pyparadigm's _SublimeBrackets_ and _SublimeTagmatcher_ (both are no longer available). I 11 | forked this to fix some issues I had and to add some features I had wanted. I also wanted to improve the efficiency of 12 | the matching. 13 | 14 | Moving forward, I have thrown away all of the code and have completely rewritten the entire code base to allow for a 15 | more flexibility, faster, and more feature rich experience. 16 | 17 | ![screenshot](docs/src/markdown/images/Example1.png) 18 | 19 | # Feature List 20 | 21 | - Customizable to highlight almost any bracket. 22 | - Customizable bracket highlight style. 23 | - High visibility bracket highlight mode. 24 | - Selectively disable or enable specific matching of tags, brackets, or quotes. 25 | - Selectively use an allowlist or blocklist for matching specific tags, brackets, or quotes based on language. 26 | - When bound to a shortcut, allow option to show line count and char count between match in the status bar. 27 | - Highlight basic brackets within strings. 28 | - Works with multi-select. 29 | - Configurable custom gutter icons. 30 | - Toggle bracket escape mode for string brackets (regex|string). 31 | - Bracket plugins that can jump between bracket ends, select content, remove brackets and/or content, wrap selections 32 | with brackets, swap brackets, swap quotes (handling quote escaping between the main quotes), fold/unfold content 33 | between brackets, toggle through tag attribute selection, select both the opening and closing tag name to change both 34 | simultaneously, etc. 35 | 36 | # Documentation 37 | 38 | https://facelessuser.github.io/BracketHighlighter/ 39 | 40 | # License 41 | 42 | Released under the MIT license. 43 | 44 | [github-ci-image]: https://github.com/facelessuser/BracketHighlighter/workflows/build/badge.svg?branch=master&event=push 45 | [github-ci-link]: https://github.com/facelessuser/BracketHighlighter/actions?query=workflow%3Abuild+branch%3Amaster 46 | [pc-image]: https://img.shields.io/packagecontrol/dt/BracketHighlighter.svg?labelColor=333333&logo=sublime%20text 47 | [pc-link]: https://packagecontrol.io/packages/BracketHighlighter 48 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333 49 | [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal 50 | [donate-link]: https://www.paypal.me/facelessuser 51 | -------------------------------------------------------------------------------- /bh_modules/rubykeywords.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import re 8 | 9 | RE_DEF = re.compile(r"\s*(?:(?:private|public|protected)\s+)?(def).*?") 10 | RE_DEF_ENDLESS = re.compile( 11 | r"^[ \t]*((?:private|public|protected)\s+)?def\s+(?:[\w\.=]+)[?!]?\s*(\([^)]+\)\s*|\s+)=", 12 | re.M 13 | ) 14 | RE_KEYWORD = re.compile(r"(\s*\b)[\w\W]*") 15 | SPECIAL_KEYWORDS = ('do',) 16 | NORMAL_KEYWORDS = ('for', 'until', 'unless', 'while', 'class', 'module', 'if', 'begin', 'case') 17 | RE_PREVIOUS = re.compile(r'.*([^\s])\s*=$') 18 | 19 | 20 | def validate(view, name, bracket, bracket_side, bfr): 21 | """Check if bracket is lowercase.""" 22 | 23 | b = bfr[bracket.begin:bracket.end] 24 | if bracket_side == 1 and b.strip() == '=': 25 | if view.match_selector(bracket.end, 'meta.function.parameters.default-value.ruby, string, comment'): 26 | return False 27 | view.score_selector 28 | left = max(0, bracket.begin - 1) 29 | previous = bfr[left:bracket.end] 30 | m = RE_PREVIOUS.match(previous) 31 | if m: 32 | s = left + m.start(1) 33 | selector = 'meta.function.ruby entity.name.function.ruby, meta.function.parameters.ruby' 34 | if not view.match_selector(s, selector): 35 | return False 36 | return True 37 | 38 | 39 | def compare(name, first, second, bfr): 40 | """Differentiate 'repeat..until' from '*..end' brackets.""" 41 | 42 | brackets = (bfr[first.begin:first.end], bfr[second.begin:second.end]) 43 | if brackets[1].lstrip() == '=' and RE_DEF.match(brackets[0]): 44 | return True 45 | if brackets[1] == 'end': 46 | return True 47 | return False 48 | 49 | 50 | def post_match(view, name, style, first, second, center, bfr, threshold): 51 | """Strip whitespace from being targeted with highlight.""" 52 | 53 | if first is not None: 54 | # Strip whitespace from the beginning of first bracket 55 | open_bracket = bfr[first.begin:first.end] 56 | if open_bracket not in SPECIAL_KEYWORDS: 57 | open_bracket_stripped = open_bracket.strip() 58 | if open_bracket_stripped not in NORMAL_KEYWORDS: 59 | m = RE_DEF.match(open_bracket) 60 | if m: 61 | first = first.move(first.begin + m.start(1), first.begin + m.end(1)) 62 | if second and bfr[second.begin:second.end].lstrip() == '=': 63 | second = first 64 | else: 65 | m = RE_KEYWORD.match(open_bracket) 66 | if m: 67 | first = first.move(first.begin + m.end(1), first.end) 68 | return first, second, style 69 | -------------------------------------------------------------------------------- /bh_swapping.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2020 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from . import bh_wrapping 10 | 11 | 12 | class SwapBrackets(bh_wrapping.WrapBrackets): 13 | """ 14 | Swap Base Class. 15 | 16 | Swap base is derived from the wrap base. 17 | """ 18 | 19 | def wrap(self, wrap_entry): 20 | """Setup for wrapping.""" 21 | 22 | if wrap_entry < 0: 23 | return 24 | 25 | self._style = ["inline"] 26 | 27 | self.brackets = self._brackets[wrap_entry] 28 | self.wrap_brackets(0) 29 | 30 | 31 | class SwapBracketsCommand(sublime_plugin.TextCommand): 32 | """Swap bracket command.""" 33 | 34 | def finalize(self, callback): 35 | """Execute post wrap callback.""" 36 | 37 | if self.view is not None: 38 | if not self.view.settings().get("bracket_highlighter.busy", False): 39 | callback() 40 | else: 41 | sublime.set_timeout(lambda: self.finalize(callback), 100) 42 | 43 | def swap_brackets(self, value): 44 | """Swap the brackets.""" 45 | 46 | if value < 0: 47 | return 48 | 49 | self.brackets = self.wrap._brackets[value] 50 | 51 | self.view.run_command( 52 | "bh_async_key" if self._async else "bh_key", 53 | { 54 | "plugin": { 55 | "type": ["__all__"], 56 | "command": "bh_modules.swapbrackets" 57 | } 58 | } 59 | ) 60 | 61 | self.view = self.window.active_view() 62 | 63 | if self._async: 64 | sublime.set_timeout(lambda: self.finalize(lambda: self.wrap.wrap(value)), 100) 65 | else: 66 | self.finalize(self.wrap.wrap(value)) 67 | 68 | def run(self, edit, **kwargs): 69 | """Initiate the swap.""" 70 | 71 | self._async = kwargs.get('async', False) 72 | self.window = self.view.window() 73 | self.wrap = SwapBrackets(self.view, "bh_swapping.sublime-settings", "swapping") 74 | 75 | if len(self.wrap._menu): 76 | self.window.show_quick_panel( 77 | self.wrap._menu, 78 | self.swap_brackets 79 | ) 80 | 81 | def is_enabled(self, **kwargs): 82 | """Check if command is enabled.""" 83 | 84 | settings = self.view.settings() 85 | return bool( 86 | not settings.get('bracket_highlighter.ignore', False) and 87 | ( 88 | not settings.get('is_widget') or 89 | sublime.load_settings("bh_core.sublime-settings").get('search_in_widgets', False) 90 | ) 91 | ) 92 | -------------------------------------------------------------------------------- /docs/src/markdown/extended-regex.md: -------------------------------------------------------------------------------- 1 | # Extended Regex Guide 2 | 3 | ## Overview 4 | 5 | BH uses Python's Re regular expression engine, but it also adds some additional back references to aid in the creation 6 | of bracket patterns. This is done with a custom wrapper called Backrefs that was originally written for 7 | [RegReplace][regreplace]. 8 | 9 | Backrefs was written to add various additional back references that are known to some regex engines, but not to Python's 10 | Re. Backrefs adds: `\p`, `\P`, `\u`, `\U`, `\l`, `\L`, `\Q` or `\E` (though `\u` and `\U` are replaced with `\c` and 11 | `\C`). 12 | 13 | You can read more about Backrefs' features in [Backrefs' documentation][backrefs]. 14 | 15 | ## Getting the Latest Backrefs 16 | 17 | It is not always clear when Package Control updates dependencies. So to force dependency updates, you can run Package 18 | Control's `Satisfy Dependencies` command which will update to the latest release. 19 | 20 | ## Using Backrefs in BracketHighlighter Plugins 21 | 22 | You can import Backrefs into a `bh_plugin`: 23 | 24 | ```python 25 | from backrefs as bre 26 | ``` 27 | 28 | Backrefs does provide a wrapper for all of Re's normal functions such as `match`, `sub`, etc., but it is recommended to 29 | pre-compile your search patterns **and** your replace patterns for the best performance; especially if you plan on 30 | reusing the same pattern multiple times. As Re does cache a certain amount of the non-compiled calls, you will be 31 | spared from some of the performance hit, but Backrefs does not cache the pre-processing of search and replace patterns. 32 | 33 | To use pre-compiled functions, you compile the search pattern with `compile_search`. If you want to take advantage of 34 | replace Backrefs, you need to compile the replace pattern as well. Notice the compiled pattern is fed into the replace 35 | pattern; you can feed the replace compiler the string representation of the search pattern as well, but the compiled 36 | pattern will be faster and is the recommended way. 37 | 38 | ```py 39 | pattern = bre.compile_search(r'somepattern', flags) 40 | replace = bre.compile_replace(pattern, r'\1 some replace pattern') 41 | ``` 42 | 43 | Then you can use the complied search pattern and replace 44 | 45 | ```py 46 | text = pattern.sub(replace, r'sometext') 47 | ``` 48 | 49 | or 50 | 51 | ```py 52 | m = pattern.match(r'sometext') 53 | if m: 54 | text = replace(m) # similar to m.expand(template) 55 | ``` 56 | 57 | To use the non-compiled search/replace functions, you call them just them as you would in Re; the names are the same. 58 | Methods like `sub` and `subn` will compile the replace pattern on the fly if given a string. 59 | 60 | ```python 61 | for m in bre.finditer(r'somepattern', 'some text', bre.UNICODE | bre.DOTALL): 62 | # do something 63 | ``` 64 | 65 | If you want to replace without compiling, you can use the `expand` method. 66 | 67 | ```python 68 | m = bre.match(r'sometext') 69 | if m: 70 | text = bre.expand(m, r'replace pattern') 71 | ``` 72 | -------------------------------------------------------------------------------- /bh_swapping.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "swapping": [ 3 | { 4 | "enabled": true, "language_list": [], "language_filter": "allowlist", "entries": [ 5 | {"name": "<> Angle", "brackets": ["<", ">${BH_SEL}"]} 6 | ] 7 | }, 8 | { 9 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 10 | {"name": "{} Curly", "brackets": ["{", "}${BH_SEL}"]} 11 | ] 12 | }, 13 | { 14 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 15 | {"name": "() Round", "brackets": ["(", ")${BH_SEL}"]} 16 | ] 17 | }, 18 | { 19 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 20 | {"name": "[] Square", "brackets": ["[", "]${BH_SEL}"]} 21 | ] 22 | }, 23 | { 24 | "enabled": true, "language_list": ["HTML", "HTML 5", "XML", "PHP", "CFML", "HTML+CFML", "ColdFusion", "ColdFusionCFC"], "language_filter": "allowlist", "entries": [ 25 | {"name": "HTML/XML Tag", "brackets": ["<${BH_SEL:NAME}>", ""]} 26 | ] 27 | }, 28 | { 29 | "enabled": true, "language_list": ["Markdown", "Multimarkdown", "GithubFlavoredMarkdown", "Markdown Extended"], "language_filter": "allowlist", "entries": [ 30 | {"name": "Markdown: Bold", "brackets": ["**", "**${BH_SEL}"]}, 31 | {"name": "Markdown: Italic", "brackets": ["_", "_${BH_SEL}"]}, 32 | {"name": "Markdown: Monospace", "brackets": ["`", "`${BH_SEL}"]} 33 | ] 34 | }, 35 | { 36 | "enabled": true, "language_list": ["LaTeX", "LaTeX (TikZ)", "knitr (Rnw)"], "language_filter": "allowlist", "entries": [ 37 | {"name": "LaTeX Environment", "brackets": ["\\begin{${BH_SEL:NAME}}", "\\end{${BH_SEL:NAME}}"]} 38 | ] 39 | }, 40 | { 41 | "enabled": true, "language_list": ["C++", "C", "C Improved"], "language_filter": "allowlist", "entries": [ 42 | {"name": "C/C++: #if", "brackets": ["#if ${BH_SEL}", "#endif"]}, 43 | {"name": "C/C++: #if, #else", "brackets": ["#if${BH_SEL}", "#else\n${BH_TAB:/* CODE */}\n#endif"]}, 44 | {"name": "C/C++: #if, #elif", "brackets": ["#if${BH_SEL}", "#elif ${BH_TAB:/* CONDITION */}\n${BH_TAB:/* CODE */}\n#endif"]}, 45 | {"name": "C/C++: #ifdef", "brackets": ["#ifdef${BH_SEL}", "#endif"]}, 46 | {"name": "C/C++: #ifdef, #else", "brackets": ["#ifdef${BH_SEL}", "#else\n${BH_TAB:/* CODE */}\n#endif"]}, 47 | {"name": "C/C++: #ifndef", "brackets": ["#ifndef${BH_SEL}", "#endif"]}, 48 | {"name": "C/C++: #ifndef, #else", "brackets": ["#ifndef${BH_SEL}", "#else\n${BH_TAB:/* CODE */}\n#endif"]} 49 | ] 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /.pyspelling.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: settings 3 | sources: 4 | - '**/*.sublime-settings' 5 | aspell: 6 | lang: en 7 | dictionary: 8 | wordlists: 9 | - docs/src/dictionary/en-custom.txt 10 | output: build/dictionary/settings.dic 11 | pipeline: 12 | - pyspelling.filters.cpp: 13 | prefix: 'st' 14 | group_comments: true 15 | line_comments: false 16 | - pyspelling.filters.context: 17 | context_visible_first: true 18 | escapes: '\\[\\`]' 19 | delimiters: 20 | # Ignore multiline content between fences (fences can have 3 or more back ticks) 21 | # ``` 22 | # content 23 | # ``` 24 | - open: '(?s)^(?P *`{3,})$' 25 | close: '^(?P=open)$' 26 | # Ignore text between inline back ticks 27 | - open: '(?P`+)' 28 | close: '(?P=open)' 29 | - pyspelling.filters.url: 30 | 31 | - name: mkdocs 32 | sources: 33 | - site/**/*.html 34 | aspell: 35 | lang: en 36 | dictionary: 37 | wordlists: 38 | - docs/src/dictionary/en-custom.txt 39 | output: build/dictionary/mkdocs.dic 40 | pipeline: 41 | - pyspelling.filters.html: 42 | comments: false 43 | attributes: 44 | - title 45 | - alt 46 | ignores: 47 | - 'code, pre, a.magiclink, span.keys' 48 | - '.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon' 49 | - '.md-social__link' 50 | - pyspelling.filters.url: 51 | 52 | - name: markdown 53 | sources: 54 | - readme.md 55 | aspell: 56 | lang: en 57 | dictionary: 58 | wordlists: 59 | - docs/src/dictionary/en-custom.txt 60 | output: build/dictionary/markdown.dic 61 | pipeline: 62 | - pyspelling.filters.markdown: 63 | - pyspelling.filters.html: 64 | comments: false 65 | attributes: 66 | - title 67 | - alt 68 | ignores: 69 | - :is(code, pre) 70 | - pyspelling.filters.url: 71 | 72 | - name: python 73 | sources: 74 | - './**/*.py' 75 | aspell: 76 | lang: en 77 | dictionary: 78 | wordlists: 79 | - docs/src/dictionary/en-custom.txt 80 | output: build/dictionary/python.dic 81 | pipeline: 82 | - pyspelling.filters.python: 83 | group_comments: true 84 | - pyspelling.flow_control.wildcard: 85 | allow: 86 | - py-comment 87 | - pyspelling.filters.context: 88 | context_visible_first: true 89 | delimiters: 90 | # Ignore lint (noqa) and coverage (pragma) as well as shebang (#!) 91 | - open: '^(?: *(?:noqa\b|pragma: no cover)|!)' 92 | close: '$' 93 | # Ignore Python encoding string -*- encoding stuff -*- 94 | - open: '^ *-\*-' 95 | close: '-\*-$' 96 | - pyspelling.filters.context: 97 | context_visible_first: true 98 | escapes: '\\[\\`]' 99 | delimiters: 100 | # Ignore multiline content between fences (fences can have 3 or more back ticks) 101 | # ``` 102 | # content 103 | # ``` 104 | - open: '(?s)^(?P *`{3,})$' 105 | close: '^(?P=open)$' 106 | # Ignore text between inline back ticks 107 | - open: '(?P`+)' 108 | close: '(?P=open)' 109 | - pyspelling.filters.url: 110 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | template: 'facelessuser:master-labels:labels.yml:master' 2 | 3 | # Wildcard labels 4 | 5 | brace_expansion: true 6 | extended_glob: true 7 | minus_negate: false 8 | 9 | rules: 10 | - labels: ['C: infrastructure'] 11 | patterns: ['*|!@(*.md|*.py|*.sublime-@(keymap|menu|settings|commands))', '@(requirements|.github)/**'] 12 | 13 | - labels: ['C: source'] 14 | patterns: ['**/@(*.py|*.sublime-@(keymap|menu|settings|commands))|!tests'] 15 | 16 | - labels: ['C: docs'] 17 | patterns: ['**/*.md|docs/**'] 18 | 19 | - labels: ['C: tests'] 20 | patterns: ['tests/**'] 21 | 22 | - labels: ['C: plugins'] 23 | patterns: ['bh_modules/**|bh_plugin.py'] 24 | 25 | - labels: ['C: swapping'] 26 | patterns: ['**/*swapping*'] 27 | 28 | - labels: ['C: wrapping'] 29 | patterns: ['**/*wrapping*'] 30 | 31 | - labels: ['C: core'] 32 | patterns: ['**/*core*'] 33 | 34 | - labels: ['C: logging'] 35 | patterns: ['bh_logging.py'] 36 | 37 | - labels: ['C: popups'] 38 | patterns: ['bh_popup.py'] 39 | 40 | - labels: ['C: regions'] 41 | patterns: ['bh_regions.py'] 42 | 43 | - labels: ['C: remove'] 44 | patterns: ['bh_remove.py'] 45 | 46 | - labels: ['C: rules'] 47 | patterns: ['bh_rules.py'] 48 | 49 | - labels: ['C: search'] 50 | patterns: ['bh_search.py'] 51 | 52 | - labels: ['C: tags'] 53 | patterns: ['**/bh_modules/tag@(s|attrselect|nameselect).py|bh_tag.sublime-settings'] 54 | 55 | - labels: ['C: settings'] 56 | patterns: ['*.sublime-@(keymap|menu|settings|commands)'] 57 | 58 | - labels: ['C: icons'] 59 | patterns: ['**/icons/*'] 60 | 61 | # Label management 62 | 63 | labels: 64 | - name: 'C: plugins' 65 | renamed: plugins 66 | color: subcategory 67 | description: Related to plugins. 68 | 69 | - name: 'C: swapping' 70 | renamed: swapping 71 | color: subcategory 72 | description: Related to the swapping library. 73 | 74 | - name: 'C: wrapping' 75 | renamed: wrapping 76 | color: subcategory 77 | description: Related to the wrapping library. 78 | 79 | - name: 'C: core' 80 | renamed: core 81 | color: subcategory 82 | description: Related to the core code. 83 | 84 | - name: 'C: logging' 85 | renamed: logging 86 | color: subcategory 87 | description: Related to logging 88 | 89 | - name: 'C: popups' 90 | renamed: popups 91 | color: subcategory 92 | description: Related to popups 93 | 94 | - name: 'C: regions' 95 | renamed: regions 96 | color: subcategory 97 | description: Related to regions. 98 | 99 | - name: 'C: remove' 100 | renamed: remove 101 | color: subcategory 102 | description: Related to bracket removal. 103 | 104 | - name: 'C: rules' 105 | renamed: rules 106 | color: subcategory 107 | description: Related to bracket rules. 108 | 109 | - name: 'C: search' 110 | renamed: search 111 | color: subcategory 112 | description: Related to search. 113 | 114 | - name: 'C: tags' 115 | renamed: tags 116 | color: subcategory 117 | description: Related to tag handling. 118 | 119 | - name: 'C: settings' 120 | renamed: settings 121 | color: subcategory 122 | description: Related to Sublime settings files. 123 | 124 | - name: 'C: icons' 125 | renamed: icons 126 | color: subcategory 127 | description: Bracket icons. 128 | -------------------------------------------------------------------------------- /bh_modules/tagattrselect.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | from BracketHighlighter import bh_plugin 9 | from BracketHighlighter.bh_plugin import import_module 10 | tags = import_module("bh_modules.tags") 11 | 12 | 13 | class SelectAttr(bh_plugin.BracketPluginCommand): 14 | """Select attribute plugin.""" 15 | 16 | def run(self, edit, name, direction='right'): 17 | """ 18 | Select next attribute in the given direction. 19 | 20 | Wrap when the end is hit. 21 | """ 22 | 23 | if self.left.size() <= 1: 24 | return 25 | tag_settings = sublime.load_settings("bh_tag.sublime-settings") 26 | tag_mode = tags.get_tag_mode(self.view, tag_settings.get("tag_mode", [])) 27 | tag_name = tag_settings.get('tag_name')[tag_mode] 28 | attr_name = tag_settings.get('attributes')[tag_mode] 29 | tname = self.view.find(tag_name, self.left.begin) 30 | current_region = self.selection[0] 31 | current_pt = self.selection[0].b 32 | region = self.view.find(attr_name, tname.b) 33 | selection = self.selection 34 | 35 | if direction == 'left': 36 | last = None 37 | 38 | # Keep track of last attribute 39 | if region is not None and current_pt <= region.b and region.b < self.left.end: 40 | last = region 41 | 42 | while region is not None and region.b < self.left.end: 43 | # Select attribute until you have closest to the left of selection 44 | if ( 45 | current_pt > region.b or 46 | ( 47 | current_pt <= region.b and current_region.a >= region.a and not 48 | ( 49 | region.a == current_region.a and region.b == current_region.b 50 | ) 51 | ) 52 | ): 53 | selection = [region] 54 | last = None 55 | # Update last attribute 56 | elif last is not None: 57 | last = region 58 | region = self.view.find(attr_name, region.b) 59 | # Wrap right 60 | if last is not None: 61 | selection = [last] 62 | else: 63 | first = None 64 | # Keep track of first attribute 65 | if region is not None and region.b < self.left.end: 66 | first = region 67 | 68 | while region is not None and region.b < self.left.end: 69 | # Select closest attribute to the right of the selection 70 | if ( 71 | current_pt < region.b or 72 | ( 73 | current_pt <= region.b and current_region.a >= region.a and not 74 | ( 75 | region.a == current_region.a and region.b == current_region.b 76 | ) 77 | ) 78 | ): 79 | selection = [region] 80 | first = None 81 | break 82 | region = self.view.find(attr_name, region.b) 83 | # Wrap left 84 | if first is not None: 85 | selection = [first] 86 | self.selection = selection 87 | 88 | 89 | def plugin(): 90 | """Make plugin available.""" 91 | 92 | return SelectAttr 93 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: BracketHighlighter Documentation 2 | site_url: https://facelessuser.github.io/BracketHighlighter 3 | repo_url: https://github.com/facelessuser/BracketHighlighter 4 | edit_uri: tree/master/docs/src/markdown 5 | site_description: A customizable Sublime Text plugin that highlights matching brackets. 6 | copyright: | 7 | Copyright © 2013 - 2025 Isaac Muse 8 | 9 | docs_dir: docs/src/markdown 10 | theme: 11 | name: material 12 | custom_dir: docs/theme 13 | icon: 14 | logo: material/book-open-page-variant 15 | palette: 16 | scheme: dracula 17 | primary: deep purple 18 | accent: deep purple 19 | font: 20 | text: Roboto 21 | code: Roboto Mono 22 | features: 23 | - navigation.tabs 24 | - navigation.top 25 | 26 | nav: 27 | - Home: 28 | - BracketHighlighter: index.md 29 | - Installation: installation.md 30 | - Basic Usage: usage.md 31 | - Customizing: customize.md 32 | - Extended Regex Guide: extended-regex.md 33 | - About: 34 | - Contributing & Support: about/contributing.md 35 | - Changelog: https://github.com/facelessuser/BracketHighlighter/blob/master/CHANGES.md 36 | - License: about/license.md 37 | 38 | markdown_extensions: 39 | - markdown.extensions.toc: 40 | slugify: !!python/name:pymdownx.slugs.uslugify 41 | permalink: "" 42 | - markdown.extensions.smarty: 43 | smart_quotes: false 44 | - pymdownx.betterem: 45 | - markdown.extensions.attr_list: 46 | - markdown.extensions.tables: 47 | - markdown.extensions.abbr: 48 | - markdown.extensions.footnotes: 49 | - markdown.extensions.md_in_html: 50 | - pymdownx.superfences: 51 | - pymdownx.highlight: 52 | extend_pygments_lang: 53 | - name: php-inline 54 | lang: php 55 | options: 56 | startinline: true 57 | - name: pycon3 58 | lang: pycon 59 | options: 60 | python3: true 61 | - pymdownx.inlinehilite: 62 | - pymdownx.magiclink: 63 | repo_url_shortener: true 64 | repo_url_shorthand: true 65 | social_url_shorthand: true 66 | user: facelessuser 67 | repo: BracketHighlighter 68 | - pymdownx.tilde: 69 | - pymdownx.caret: 70 | - pymdownx.smartsymbols: 71 | - pymdownx.emoji: 72 | emoji_index: !!python/name:material.extensions.emoji.twemoji 73 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 74 | - pymdownx.escapeall: 75 | hardbreak: true 76 | nbsp: true 77 | - pymdownx.tasklist: 78 | custom_checkbox: true 79 | - pymdownx.arithmatex: 80 | - pymdownx.mark: 81 | - pymdownx.striphtml: 82 | - pymdownx.snippets: 83 | base_path: 84 | - docs/src/markdown/.snippets 85 | auto_append: 86 | - refs.md 87 | - pymdownx.keys: 88 | separator: "\uff0b" 89 | - pymdownx.saneheaders: 90 | - pymdownx.blocks.admonition: 91 | types: 92 | - new 93 | - settings 94 | - note 95 | - abstract 96 | - info 97 | - tip 98 | - success 99 | - question 100 | - warning 101 | - failure 102 | - danger 103 | - bug 104 | - example 105 | - quote 106 | - pymdownx.blocks.details: 107 | types: 108 | - name: details-new 109 | class: new 110 | - name: details-settings 111 | class: settings 112 | - name: details-note 113 | class: note 114 | - name: details-abstract 115 | class: abstract 116 | - name: details-info 117 | class: info 118 | - name: details-tip 119 | class: tip 120 | - name: details-success 121 | class: success 122 | - name: details-question 123 | class: question 124 | - name: details-warning 125 | class: warning 126 | - name: details-failure 127 | class: failure 128 | - name: details-danger 129 | class: danger 130 | - name: details-bug 131 | class: bug 132 | - name: details-example 133 | class: example 134 | - name: details-quote 135 | class: quote 136 | - pymdownx.blocks.html: 137 | - pymdownx.blocks.definition: 138 | - pymdownx.blocks.tab: 139 | alternate_style: True 140 | - pymdownx.fancylists: 141 | inject_style: true 142 | - pymdownx.blocks.caption: 143 | 144 | extra: 145 | social: 146 | - icon: fontawesome/brands/github 147 | link: https://github.com/facelessuser 148 | 149 | plugins: 150 | - search 151 | - git-revision-date-localized 152 | - mkdocs_pymdownx_material_extras 153 | - minify: 154 | minify_html: true 155 | -------------------------------------------------------------------------------- /quickstart.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ![screenshot](res://Packages/BracketHighlighter/docs/src/markdown/images/Example1.png){: width=769, height=270} 4 | 5 | BracketHighlighter is designed to auto highlight your brackets in various source files. In general, if you are using 6 | one of the common languages, BracketHighlighter will work out of the box. 7 | 8 | It is advised that you disable Sublime's default bracket and tag matcher in your `Preferences.sublime-settings` file 9 | or you will have matching conflicts: 10 | 11 | ```js 12 | "match_brackets": false, 13 | "match_brackets_angle": false, 14 | "match_brackets_braces": false, 15 | "match_brackets_content": false, 16 | "match_brackets_square": false, 17 | "match_tags": false 18 | ``` 19 | 20 | BracketHighlighter also contains a number 21 | of extensions that allows for deleting, replacing, or even wrapping brackets; check out the [documentation](http://facelessuser.github.io/BracketHighlighter/usage/) to learn 22 | more about basic usage. 23 | 24 | BracketHighlighter (if you are on the latest version of Sublime Text 3) also will show a popup on screen if you mouse 25 | over a bracket and the other matching pair is offscreen. The popup will show the offscreen bracket with surrounding 26 | lines and allow you to jump to the offscreen bracket. 27 | 28 | ![popup](res://Packages/BracketHighlighter/docs/src/markdown/images/popup1.png){: width=300, height=179} 29 | 30 | # Where It Doesn't Work 31 | 32 | BracketHighlighter excludes plain text by default. The free form nature of plain text and lack of syntax highlight 33 | scoping makes it more difficult to detect intended brackets. 34 | 35 | Some language might not be supported yet, but they can be added via pull requests. Check out the [documentation](http://facelessuser.github.io/BracketHighlighter/customize/#configuring-brackets) to 36 | learn about adding bracket rules and take a look at the default [settings file](sub://Packages/BracketHighlighter/bh_core.sublime-settings) to see examples. 37 | 38 | # My Language Isn't Supported 39 | 40 | BracketHighlighter supports numerous different languages and specialty bracket types, but your language might not be 41 | supported yet. The most common requested enhancement for BracketHighlighter is for new rules to support for a new a 42 | previously unsupported language. I, like you, am proficient in very specific languages. I probably don’t use your 43 | favorite language or there would already be a support for it. I don’t have time to learn the nuances of your language. 44 | For these reasons, support for new language brackets requires pull requests from the community. 45 | 46 | Though I will not personally implement rules for your favorite language, I am more than willing to offer suggestions 47 | and guidance to help those who may struggle to create rules for their specific language of interest. 48 | 49 | If you find a bug in a supported language I am familiar with, I am happy to address the issue. If the bug is found in 50 | a language I am not proficient in, it is likely I will defer fixes to the community. I may offer suggestions and 51 | encourage the issue creator to do the actual testing as they will be more familiar with the language. 52 | 53 | # Customizing 54 | 55 | BracketHighlighter can be tweaked to show specific brackets with specific colors and styles. Due to the way sublime 56 | handles colored regions, the method of specifying specific colors can be cumbersome, but it is all [documented](http://facelessuser.github.io/BracketHighlighter/customize/#configuring-highlight-style). 57 | 58 | # Sometimes I See a Question Mark 59 | 60 | BracketHighlighter's auto highlighting does not scan the entire file all at once, but it scans a small window for 61 | performance reasons. If you see a question mark, it may be simply that the search threshold has been reached. You 62 | can run a bracket match with no threshold from the command palette or the offscreen bracket popup dialog (if using 63 | the latest version of Sublime Text 3). If you are dissatisfied with the default small threshold, it can be increased 64 | in the settings file. Be mindful that extremely large thresholds may affect performance. I personally use a value of 65 | `10000`. 66 | 67 | ![unmatched](res://Packages/BracketHighlighter/docs/src/markdown/images/unmatched_popup.png){: width=486, height=210} 68 | 69 | # I Need Help! 70 | 71 | That's okay. Bugs are sometimes introduced or discovered in existing code. Sometimes the documentation isn't clear. 72 | Support can be found over on the [official repo](https://github.com/facelessuser/BracketHighlighter/issues). Make sure to first search the documentation and previous issues 73 | before opening a new issue. And when creating a new issue, make sure to fill in the provided issue template. Please 74 | be courteous and kind in your interactions. 75 | -------------------------------------------------------------------------------- /bh_tag.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | /* If you add a new key to one of these settings, you must add one to all! */ 3 | 4 | // This is where you initially define a new tag mode. 5 | // This new tag_mode key must be used in all other settings in this file 6 | // to set up its specific settings. 7 | // Determine which style of tag-matching to use in which syntax. 8 | "tag_mode": [ 9 | {"mode": "xml", "syntax": ["XML"]}, 10 | { 11 | "mode": "xhtml", 12 | "syntax": [ 13 | "HTML", 14 | "HTML 5", 15 | "PHP", 16 | "HTML (Jinja Templates)", 17 | "HTML (Jinja2)", 18 | "HTML (Rails)", 19 | "HTML (Twig)", 20 | "HTML (Django)", 21 | "Jinja", 22 | "laravel-blade", 23 | "blade", 24 | "Handlebars", 25 | "AngularJS", 26 | "Java Server Pages (JSP)" 27 | ], 28 | "first_line": "^[ \\t]*<\\?xml" 29 | }, 30 | { 31 | "mode": "html", 32 | "syntax": [ 33 | "HTML", 34 | "HTML 5", 35 | "PHP", 36 | "HTML (Jinja Templates)", 37 | "HTML (Jinja2)", 38 | "HTML (Rails)", 39 | "HTML (Twig)", 40 | "HTML (Django)", 41 | "Jinja", 42 | "laravel-blade", 43 | "blade", 44 | "Handlebars", 45 | "AngularJS", 46 | "Java Server Pages (JSP)" 47 | ] 48 | }, 49 | { 50 | "mode": "cfml", 51 | "syntax": ["CFML", "HTML+CFML", "ColdFusion", "ColdFusionCFC"] 52 | } 53 | ], 54 | 55 | // Style to use for matched tags 56 | "tag_style": { 57 | "xml": "tag", 58 | "xhtml": "tag", 59 | "html": "tag", 60 | "cfml": "tag" 61 | }, 62 | 63 | // Scopes to exclude from tag searches. 64 | "tag_scope_exclude": { 65 | "xhtml": ["string", "comment"], 66 | "html": ["string", "comment"], 67 | "cfml": ["string", "comment"] 68 | }, 69 | 70 | // Optional closing HTML tags. You can use 'null' if it does not require a pattern. 71 | "optional_tag_patterns": { 72 | "xml": null, 73 | "xhtml": null, 74 | "html": "colgroup|dd|dt|li|option|optgroup|p|td|tfoot|th|thead|tr", 75 | "cfml": "cf.+|colgroup|dd|dt|li|option|optgroup|p|td|tfoot|th|thead|tr" 76 | }, 77 | 78 | // Tags that never have a closing. You can use 'null' if it does not require a pattern. 79 | "void_tag_patterns": { 80 | "xml": null, 81 | "xhtml": null, 82 | "html": "area|base|basefont|br|col|embed|frame|hr|img|input|isindex|keygen|link|meta|param|source|track|wbr", 83 | "cfml": "area|base|basefont|br|col|embed|frame|hr|img|input|isindex|keygen|link|meta|param|source|track|wbr" 84 | }, 85 | 86 | // Self closing tags. Single tags that are closed like this 87 | "self_closing_tag_patterns": { 88 | "xml": "[\\w:\\.\\-]+", 89 | "xhtml": "[\\w:\\.\\-]+", 90 | "html": "circle|ellipse|line|path|polygon|polyline|rect|stop|use", 91 | "cfml": "cf.+" 92 | }, 93 | 94 | // Regex for tag name. Do not use capturing groups. 95 | "tag_name": 96 | { 97 | "xml": "[\\w:\\.\\-]+", 98 | "xhtml": "[\\w:\\.\\-]+", 99 | "html": "[\\w:\\.\\-]+", 100 | "cfml": "[\\w:\\.\\-]+" 101 | }, 102 | 103 | // HTML attributes. Do not use capturing groups. 104 | "attributes": 105 | { 106 | "xml": "[\\w:][-\\w\\d:.]*(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'))?", 107 | "xhtml": "[\\w:@][-\\w\\d:.]*(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'))?", 108 | "html": "[\\w:@][-\\w\\d:.]*(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s\"'`=<>]+))?", 109 | "cfml": "[\\w:][-\\w\\d:.]*(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s\"'`=<>]+))?" 110 | }, 111 | 112 | // Regex for start/opening tag. Use a capturing group for tag name and self closing indicator '/' only. 113 | // Attributes and tag names are inserted using python string formatting: 114 | // the keyword 'attributes' and `tag_name` are used. 115 | "start_tag": { 116 | "xml": "<(%(tag_name)s)(?:(?:\\s+|(?<=['\"]))%(attributes)s)*\\s*(/?)>", 117 | "xhtml": "<(%(tag_name)s)(?:(?:\\s+|(?<=['\"]))%(attributes)s)*\\s*(/?)>", 118 | "html": "<(%(tag_name)s)(?:(?:\\s+|(?<=['\"]))%(attributes)s)*\\s*(/?)>", 119 | "cfml": "<(%(tag_name)s)(?:(?:(?:\\s+|(?<=['\"]))%(attributes)s)*|(?:(?<=cfif)|(?<=cfelseif))[^>]+)\\s*(/?)>" 120 | }, 121 | 122 | // Regex for end/closing tag. Only use a capturing group for name. 123 | "end_tag": { 124 | "xml": "]*>", 125 | "xhtml": "]*>", 126 | "html": "]*>", 127 | "cfml": "]*>" 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /bh_wrapping.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "wrapping": [ 3 | { 4 | "enabled": true, "language_list": [], "language_filter": "allowlist", "entries": [ 5 | {"name": "<> Angle", "brackets": ["<", ">${BH_SEL}"]} 6 | ] 7 | }, 8 | { 9 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 10 | {"name": "{} Curly", "brackets": ["{", "}${BH_SEL}"], "insert_style": ["inline", "block", "indent_block"]} 11 | ] 12 | }, 13 | { 14 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 15 | {"name": "() Round", "brackets": ["(", ")${BH_SEL}"], "insert_style": ["inline", "block", "indent_block"]} 16 | ] 17 | }, 18 | { 19 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 20 | {"name": "[] Square", "brackets": ["[", "]${BH_SEL}"], "insert_style": ["inline", "block", "indent_block"]} 21 | ] 22 | }, 23 | { 24 | "enabled": true, "language_list": ["Plain text"], "language_filter": "blocklist", "entries": [ 25 | {"name": "'' Single Quotes", "brackets": ["'", "'${BH_SEL}"], "insert_style": ["inline"]}, 26 | {"name": "\"\" Double Quotes", "brackets": ["\"", "\"${BH_SEL}"], "insert_style": ["inline"]} 27 | ] 28 | }, 29 | { 30 | "enabled": true, "language_list": ["Python", "PythonImproved"], "language_filter": "allowlist", "entries": [ 31 | {"name": "'''''' Triple Single Quotes", "brackets": ["'''", "'''${BH_SEL}"], "insert_style": ["inline", "block"]}, 32 | {"name": "\"\"\"\"\"\" Triple Double Quotes", "brackets": ["\"\"\"", "\"\"\"${BH_SEL}"], "insert_style": ["inline", "block"]} 33 | ] 34 | }, 35 | { 36 | "enabled": true, "language_list": ["HTML", "HTML 5", "XML", "PHP", "CFML", "HTML+CFML", "ColdFusion", "ColdFusionCFC"], "language_filter": "allowlist", "entries": [ 37 | {"name": "HTML/XML Tag", "brackets": ["<${BH_SEL:NAME}>", ""], "insert_style": ["inline", "block", "indent_block"]} 38 | ] 39 | }, 40 | { 41 | "enabled": true, "language_list": ["Markdown", "Multimarkdown", "GithubFlavoredMarkdown", "Markdown Extended"], "language_filter": "allowlist", "entries": [ 42 | {"name": "Markdown: Bold", "brackets": ["**", "**${BH_SEL}"]}, 43 | {"name": "Markdown: Italic", "brackets": ["_", "_${BH_SEL}"]}, 44 | {"name": "Markdown: Monospace", "brackets": ["`", "`${BH_SEL}"]} 45 | ] 46 | }, 47 | { 48 | "enabled": true, "language_list": ["LaTeX", "LaTeX (TikZ)", "knitr (Rnw)"], "language_filter": "allowlist", "entries": [ 49 | {"name": "LaTeX Environment", "brackets": ["\\begin{${BH_SEL:NAME}}", "\\end{${BH_SEL:NAME}}"], "insert_style": ["block"]} 50 | ] 51 | }, 52 | { 53 | "enabled": true, "language_list": ["C++", "C", "C Improved"], "language_filter": "allowlist", "entries": [ 54 | {"name": "C/C++: #if", "brackets": ["#if ${BH_SEL:/* CONDITION */}", "#endif"], "insert_style": ["block"]}, 55 | {"name": "C/C++: #if, #else", "brackets": ["#if ${BH_SEL:/* CONDITION */}", "#else\n${BH_TAB:/* CODE */}\n#endif"], "insert_style": ["block"]}, 56 | {"name": "C/C++: #if, #elif", "brackets": ["#if ${BH_SEL:/* CONDITION */}", "#elif ${BH_TAB:/* CONDITION */}\n${BH_TAB:/* CODE */}\n#endif"], "insert_style": ["block"]}, 57 | {"name": "C/C++: #ifdef", "brackets": ["#ifdef ${BH_SEL:/* DEFINE */}", "#endif"], "insert_style": ["block"]}, 58 | {"name": "C/C++: #ifdef, #else", "brackets": ["#ifdef ${BH_SEL:/* DEFINE */}", "#else\n${BH_TAB:/* CODE */}\n#endif"], "insert_style": ["block"]}, 59 | {"name": "C/C++: #ifndef", "brackets": ["#ifndef ${BH_SEL:/* DEFINE */}", "#endif"], "insert_style": ["block"]}, 60 | {"name": "C/C++: #ifndef, #else", "brackets": ["#ifndef ${BH_SEL:/* DEFINE */}", "#else\n${BH_TAB:/* CODE */}\n#endif"], "insert_style": ["block"]} 61 | ] 62 | }, 63 | { 64 | "enabled": true, "language_list": ["Ruby"], "language_filter": "allowlist", "entries": [ 65 | {"name": "Ruby: if", "brackets": ["if ${BH_SEL:CONDITION}", "end"], "insert_style": ["indent_block"]}, 66 | {"name": "Ruby: until", "brackets": ["until ${BH_SEL:CONDITION}", "end"], "insert_style": ["indent_block"]}, 67 | {"name": "Ruby: while", "brackets": ["while ${BH_SEL:CONDITION}", "end"], "insert_style": ["indent_block"]}, 68 | {"name": "Ruby: do", "brackets": ["do ${BH_SEL:CONDITION}", "end"], "insert_style": ["indent_block"]}, 69 | {"name": "Ruby: def", "brackets": ["def ${BH_SEL:NAME}", "end"], "insert_style": ["indent_block"]} 70 | ] 71 | }, 72 | { 73 | "enabled": true, "language_list": ["CSS"], "language_filter": "allowlist", "entries": [ 74 | {"name": "CSS: @group", "brackets": ["/* @group ${BH_SEL:NAME} */", "/* @end */"], "insert_style": ["block"]} 75 | ] 76 | } 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /Example.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // Toggle Global Enable 3 | { 4 | "keys": ["ctrl+alt+super+e"], 5 | "command": "bh_toggle_enable" 6 | }, 7 | // Search to end of file for bracket 8 | { 9 | "keys": ["ctrl+alt+super+b"], 10 | "command": "bh_key", 11 | "args": 12 | { 13 | "lines" : true 14 | } 15 | }, 16 | // Go to left bracket 17 | { 18 | "keys": ["ctrl+alt+super+up"], 19 | "command": "bh_key", 20 | "args": 21 | { 22 | "no_outside_adj": null, 23 | "no_block_mode": null, 24 | "lines" : true, 25 | "plugin": 26 | { 27 | "command": "bh_modules.bracketselect", 28 | "args": {"select": "left"} 29 | } 30 | } 31 | }, 32 | // Go to right bracket 33 | { 34 | "keys": ["ctrl+alt+super+down"], 35 | "command": "bh_key", 36 | "args": 37 | { 38 | "no_outside_adj": null, 39 | "no_block_mode": null, 40 | "lines" : true, 41 | "plugin": 42 | { 43 | "command": "bh_modules.bracketselect", 44 | "args": {"select": "right"} 45 | } 46 | } 47 | }, 48 | // Select to left bracket 49 | { 50 | "keys": ["ctrl+alt+shift+super+up"], 51 | "command": "bh_key", 52 | "args": 53 | { 54 | "no_outside_adj": null, 55 | "no_block_mode": null, 56 | "lines" : true, 57 | "plugin": 58 | { 59 | "command": "bh_modules.bracketselect", 60 | "args": {"select": "left", "extend": true} 61 | } 62 | } 63 | }, 64 | // Select to right bracket 65 | { 66 | "keys": ["ctrl+alt+shift+super+down"], 67 | "command": "bh_key", 68 | "args": 69 | { 70 | "no_outside_adj": null, 71 | "no_block_mode": null, 72 | "lines" : true, 73 | "plugin": 74 | { 75 | "command": "bh_modules.bracketselect", 76 | "args": {"select": "right", "extend": true} 77 | } 78 | } 79 | }, 80 | // Remove brackets 81 | { 82 | "keys": ["ctrl+alt+super+r"], 83 | "command": "bh_remove_brackets" 84 | }, 85 | // Toggle string escape mode for sub bracket search in strings 86 | { 87 | "keys": ["ctrl+alt+super+x"], 88 | "command": "bh_toggle_string_escape_mode" 89 | }, 90 | // Jump to matching bracket 91 | { 92 | "keys": ["ctrl+m"], 93 | "command": "bh_key", 94 | "args": { 95 | "plugin": { 96 | "args": {"select": "right", "alternate": true}, 97 | "command": "bh_modules.bracketselect" 98 | }, 99 | "lines": true, 100 | "no_outside_adj": null 101 | } 102 | }, 103 | // Select text between brackets 104 | { 105 | "keys": ["ctrl+alt+super+s"], 106 | "command": "bh_key", 107 | "args": 108 | { 109 | "no_outside_adj": null, 110 | "lines" : true, 111 | "plugin": 112 | { 113 | "command": "bh_modules.bracketselect" 114 | } 115 | } 116 | }, 117 | // Select text including brackets 118 | { 119 | "keys": ["ctrl+alt+super+d"], 120 | "command": "bh_key", 121 | "args": 122 | { 123 | "no_outside_adj": null, 124 | "lines" : true, 125 | "plugin": 126 | { 127 | "command": "bh_modules.bracketselect", 128 | "args": {"always_include_brackets": true} 129 | } 130 | } 131 | }, 132 | // Select tag name of HTML/XML tag (both opening name and closing) 133 | { 134 | "keys": ["ctrl+alt+super+t"], 135 | "command": "bh_key", 136 | "args": 137 | { 138 | "plugin": 139 | { 140 | "type": ["cfml", "html", "angle"], 141 | "command": "bh_modules.tagnameselect" 142 | } 143 | } 144 | }, 145 | // Select the attribute to the right of the cursor (will wrap inside the tag) 146 | { 147 | "keys": ["ctrl+alt+super+right"], 148 | "command": "bh_key", 149 | "args": 150 | { 151 | "plugin": 152 | { 153 | "type": ["cfml", "html", "angle"], 154 | "command": "bh_modules.tagattrselect", 155 | "args": {"direction": "right"} 156 | } 157 | } 158 | }, 159 | // Select the attribute to the left of the cursor (will wrap inside the tag) 160 | { 161 | "keys": ["ctrl+alt+super+left"], 162 | "command": "bh_key", 163 | "args": 164 | { 165 | "plugin": 166 | { 167 | "type": ["cfml", "html", "angle"], 168 | "command": "bh_modules.tagattrselect", 169 | "args": {"direction": "left"} 170 | } 171 | } 172 | }, 173 | // Convert single quote string to double quoted string and vice versa 174 | // Will handle escaping or unescaping quotes within the string 175 | { 176 | "keys": ["ctrl+alt+super+q"], 177 | "command": "bh_key", 178 | "args": 179 | { 180 | "lines" : true, 181 | "plugin": 182 | { 183 | "type": ["single_quote", "double_quote", "py_single_quote", "py_double_quote"], 184 | "command": "bh_modules.swapquotes" 185 | } 186 | } 187 | }, 188 | // Fold contents between brackets 189 | { 190 | "keys": ["ctrl+alt+super+["], 191 | "command": "bh_key", 192 | "args": 193 | { 194 | "plugin": { 195 | "command" : "bh_modules.foldbracket" 196 | } 197 | } 198 | }, 199 | // Swap brackets with another type 200 | { 201 | "keys": ["ctrl+alt+super+e"], 202 | "command": "swap_brackets" 203 | }, 204 | // Surround selection with brackets from quick panel 205 | { 206 | "keys": ["ctrl+alt+super+w"], 207 | "command": "wrap_brackets" 208 | }, 209 | // Toggle high visibility mode 210 | { 211 | "keys": ["ctrl+alt+super+v"], 212 | "command": "bh_toggle_high_visibility" 213 | } 214 | ] 215 | -------------------------------------------------------------------------------- /docs/src/markdown/usage.md: -------------------------------------------------------------------------------- 1 | # Basic Usage 2 | 3 | ## Overview 4 | 5 | Out of the box, BH will highlight brackets (or defined brackets like start and end blocks) surrounding the cursor. BH 6 | will also put opening and closing icons in the gutter of the corresponding line containing open or closing bracket. 7 | 8 | It is advised that you disable Sublime's default bracket and tag matcher in your `Preferences.sublime-settings` file or 9 | you will have matching conflicts: 10 | 11 | Sublime's bracket highlighter can clash with BH's and it advisable to disable Sublime's bracket matching visualization. 12 | If this is desired, it can be done by creating a file in your user folder with the same name as your current color 13 | scheme using the extension `.sublime-color-scheme` (even if you are overriding a `tmTheme` color scheme) and adding the 14 | following to that file: 15 | 16 | ```js 17 | { 18 | "variables": {}, 19 | "globals": 20 | { 21 | "bracket_contents_options": "none", 22 | "brackets_options": "none", 23 | "tags_options": "none" 24 | }, 25 | "rules": [] 26 | } 27 | ``` 28 | 29 | /// note | Outdated Recommendation 30 | Previously, it was recommended to disable the following features, but this also disabled bracket related features 31 | in "expand selections" and such. This is no longer recommended. 32 | 33 | ```js 34 | "match_brackets": false, 35 | "match_brackets_angle": false, 36 | "match_brackets_braces": false, 37 | "match_brackets_content": false, 38 | "match_brackets_square": false, 39 | "match_tags": false 40 | ``` 41 | /// 42 | 43 | If you are using Sublime Text build 3124+, a new feature has been added which shows a popup when you mouse over a 44 | bracket that has its matching bracket pair off screen. It will show where the other bracket is located with line 45 | context and provide a link to jump to the other bracket. When mousing over a bracket in which the match could not be 46 | found, a popup explaining why this might occur will be shown and give the option to click a link which will perform a 47 | search without thresholds to see if it can find the brackets when restraints are removed. 48 | 49 | ## Built-in Supported brackets 50 | 51 | BH supports a variety of brackets out of the box; here are some examples: 52 | 53 | - round 54 | - square 55 | - curly 56 | - angle 57 | - single and double quotes 58 | - Python single, double, and triple quotes (Unicode and raw) 59 | - Django Python templates with mixed HTML, CSS, and JavaScript 60 | - JavaScript regex 61 | - Perl regex 62 | - Ruby regex 63 | - Markdown bold, italic, and code blocks 64 | - CSSedit groups 65 | - Ruby conditional statements 66 | - C/C++ compiler switches 67 | - PHP conditional keywords 68 | - PHP angle brackets `` 69 | - Erlang conditional statements 70 | - HTML, ColdFusion, XML, and various other template tags 71 | - Bash conditional and looping constructs 72 | - Fish conditional and looping constructs 73 | - Lua 74 | - Pascal 75 | - Elixir 76 | 77 | Within supported regex and strings, BH can also highlight basic sub brackets between the matched quotes: `(), [], {}`. 78 | 79 | ## General Commands 80 | 81 | BH has a couple of additional features built-in which are found in the command palette. 82 | 83 | ### Toggle Global Enable 84 | 85 | The `bh_toggle_enable` command enables and disables BH globally. 86 | 87 | ### Toggle String Bracket Escape Mode 88 | 89 | `bh_toggle_string_escape_mode` toggles BH's recognition mode of escaped sub brackets in strings and regex. The modes 90 | are *string escape* mode and *regex escape* mode. 91 | 92 | ### Find Matching Offscreen Brackets 93 | 94 | When `show_offscreen_bracket_popup` is enabled, mousing over an on screen bracket, or invoking the `bh_offscreen_popup` 95 | command, will show a popup on the screen that reveals the location of the matching offscreen bracket(s) (only available 96 | for Sublime Text 3 versions that support this). The cursor needs to be between a matching pair of brackets. 97 | 98 | ## Bracket Plugin Commands 99 | 100 | BH is also extendable via plugins and provides a number of built-in Bracket Plugins that take advantage of BH's matching 101 | to provide additional features. Most plugin features are available via the command palette. To see how to configure 102 | shortcuts, see the [`Example.sublime-keymap`][keymap] file. 103 | 104 | ### Bracket Select Plugin 105 | 106 | The Bracket Select plugin selects the content between the brackets or moves the selection to the opening or closing 107 | bracket. Behavior is slightly modified for tags. When the `extend` argument is set to `true`, the Bracket Select 108 | plugin extends the current selection to the left/right bracket when jumping to the chosen bracket. 109 | 110 | ### Swap Brackets Plugin 111 | 112 | The Swap Brackets plugin can swap the current brackets to another type of bracket. When selected, it will displays the 113 | bracket options that are allowed for the current language. Allowed brackets are defined in `bh_swapping.sublime-settings`. 114 | 115 | ### Wrap Brackets Plugin 116 | 117 | The Wrap Brackets plugin wraps selected text with a bracket pair. When selected, it will display the bracket options 118 | that are allowed for the current language. Allowed brackets are defined in `bh_wrapping.sublime-settings`. 119 | 120 | ### Bracket Remove Plugin 121 | 122 | The Bracket Remove plugin removes the surrounding brackets around the cursor. 123 | 124 | ### Fold Bracket Plugin 125 | 126 | The Fold Bracket plugin folds the content of the current surrounding brackets. 127 | 128 | ### Swap Quotes Plugin 129 | 130 | The Swaps Quotes plugin swaps the quote style of surrounding quotes from double to single or vice versa. It also 131 | handles escaping and un-escaping of sub quotes. 132 | 133 | ### Tag Plugin 134 | 135 | The Tag plugin Provides extra logic to target and highlight XML/HTML tags. To use BH's built-in HTML highlighting in 136 | your HTML-like template language of choice, add it to the list in `bh_tag.sublime_settings`. 137 | 138 | ### Tag Attribute Select Plugin 139 | 140 | The Tag Attribute plugin can cycle through the tag attributes of the selected tag. 141 | 142 | ### Tag Name Select Plugin 143 | 144 | Tag Name Select plugin selects the opening and closing tag name of the current selected tag. 145 | 146 | ## Keyboard Shortcuts 147 | 148 | BH provides no keyboard shortcuts in order to avoid shortcut conflicts, but you can view the included 149 | [`Example.sublime-keymap`][keymap] file to get an idea how to set up your own. 150 | -------------------------------------------------------------------------------- /bh_modules/bracketselect.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from BracketHighlighter import bh_plugin 8 | import sublime 9 | 10 | DEFAULT_TAGS = ["cfml", "html", "angle"] 11 | 12 | 13 | class SelectBracket(bh_plugin.BracketPluginCommand): 14 | """Select Bracket plugin.""" 15 | 16 | def run(self, edit, name, select='', tags=None, always_include_brackets=False, alternate=False, extend=False): 17 | """ 18 | Select the content between brackets. 19 | 20 | If "extend" is set to true, extend the current 21 | selection up to (and including) the left/right bracket 22 | when jumping to the left/right bracket. 23 | 24 | If "always_include_brackets" is enabled, 25 | include the brackets as well. 26 | If the content is already selected, expand to the parent. 27 | """ 28 | 29 | self.tags = DEFAULT_TAGS if tags is None else tags 30 | self.current_left, self.current_right = self.selection[0].begin(), self.selection[0].end() 31 | first, last = self.left.end, self.right.begin 32 | 33 | if select == 'left': 34 | first, last = self.select_left(name, first, last, alternate, extend) 35 | elif select == 'right': 36 | first, last = self.select_right(name, first, last, alternate, extend) 37 | elif first == self.current_left and last == self.current_right or always_include_brackets: 38 | first, last = self.select_expand(first, last) 39 | 40 | self.selection = [sublime.Region(first, last)] 41 | 42 | def select_left(self, name, first, last, alternate, extend): 43 | """Select the left bracket.""" 44 | 45 | if name in self.tags and self.left.size() > 1: 46 | first, last = self.left.begin + 1, self.left.begin + 1 47 | if first == self.current_left and last == self.current_right: 48 | self.refresh_match = True 49 | if alternate: 50 | first, last = self.right.begin + 1, self.right.begin + 1 51 | else: 52 | first, last = self.left.begin, self.left.begin 53 | else: 54 | if extend: 55 | anchor, cursor = self.selection[0].a, self.selection[0].b 56 | 57 | if first == cursor: 58 | self.refresh_match = True 59 | if alternate: 60 | first = anchor 61 | last = self.right.begin 62 | else: 63 | first = anchor 64 | last = self.left.begin 65 | elif first == anchor: 66 | self.refresh_match = True 67 | first, last = self.left.end, self.left.end 68 | else: 69 | self.refresh_match = True 70 | first = anchor 71 | last = self.left.end 72 | else: 73 | first, last = self.left.end, self.left.end 74 | if first == self.current_left and last == self.current_right: 75 | self.refresh_match = True 76 | if alternate: 77 | first, last = self.right.begin, self.right.begin 78 | else: 79 | first, last = self.left.begin, self.left.begin 80 | return first, last 81 | 82 | def select_right(self, name, first, last, alternate, extend): 83 | """Select the right bracket.""" 84 | 85 | if self.left.end != self.right.end: 86 | if name in self.tags and self.left.size() > 1: 87 | first, last = self.right.begin + 1, self.right.begin + 1 88 | if first == self.current_left and last == self.current_right: 89 | self.refresh_match = True 90 | if alternate: 91 | first, last = self.left.begin + 1, self.left.begin + 1 92 | else: 93 | first, last = self.right.end, self.right.end 94 | else: 95 | if extend: 96 | anchor, cursor = self.selection[0].a, self.selection[0].b 97 | 98 | if last == cursor: 99 | self.refresh_match = True 100 | if alternate: 101 | first = anchor 102 | last = self.left.end 103 | else: 104 | first = anchor 105 | last = self.right.end 106 | elif last == anchor: 107 | self.refresh_match = True 108 | first, last = self.right.begin, self.right.begin 109 | else: 110 | self.refresh_match = True 111 | first = anchor 112 | last = self.right.begin 113 | else: 114 | first, last = self.right.begin, self.right.begin 115 | if first == self.current_left and last == self.current_right: 116 | self.refresh_match = True 117 | if alternate: 118 | first, last = self.left.end, self.left.end 119 | else: 120 | first, last = self.right.end, self.right.end 121 | else: 122 | # Select the first because there is no second bracket. 123 | if name in self.tags and self.left.size() > 1: 124 | first, last = self.left.begin + 1, self.left.begin + 1 125 | if first == self.current_left and last == self.current_right: 126 | self.refresh_match = True 127 | if not alternate: 128 | first, last = self.left.end, self.left.end 129 | else: 130 | first, last = self.right.end, self.right.end 131 | if first == self.current_left and last == self.current_right: 132 | self.refresh_match = True 133 | if not alternate: 134 | first, last = self.right.end, self.right.end 135 | return first, last 136 | 137 | def select_expand(self, first, last): 138 | """Expand content selection.""" 139 | 140 | first, last = self.left.begin, self.right.end 141 | self.refresh_match = True 142 | return first, last 143 | 144 | 145 | def plugin(): 146 | """Make plugin available.""" 147 | 148 | return SelectBracket 149 | -------------------------------------------------------------------------------- /bh_plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from os.path import normpath, join 10 | from collections import namedtuple 11 | import sys 12 | import traceback 13 | import re 14 | from .bh_logging import log 15 | 16 | 17 | class Payload(object): 18 | """Plugin payload.""" 19 | 20 | status = False 21 | plugin = None 22 | args = None 23 | 24 | @classmethod 25 | def clear(cls): 26 | """Clear payload.""" 27 | 28 | cls.status = False 29 | cls.plugin = None 30 | cls.args = None 31 | 32 | 33 | class BracketRegion (namedtuple('BracketRegion', ['begin', 'end'])): 34 | """Bracket regions for plugins.""" 35 | 36 | def move(self, begin, end): 37 | """Move bracket region to different points.""" 38 | 39 | return self._replace(begin=begin, end=end) 40 | 41 | def size(self): 42 | """Get the size of the region.""" 43 | 44 | return abs(self.begin - self.end) 45 | 46 | def toregion(self): 47 | """Convert to sublime region.""" 48 | 49 | return sublime.Region(self.begin, self.end) 50 | 51 | 52 | def is_bracket_region(obj): 53 | """Check if object is a `BracketRegion`.""" 54 | 55 | return isinstance(obj, BracketRegion) 56 | 57 | 58 | def sublime_format_path(pth): 59 | """Format path for Sublime internally.""" 60 | m = re.match(r"^([A-Za-z]{1}):(?:/|\\)(.*)", pth) 61 | if sublime.platform() == "windows" and m is not None: 62 | pth = m.group(1) + "/" + m.group(2) 63 | return pth.replace("\\", "/") 64 | 65 | 66 | def load_modules(obj, loaded): 67 | """Load bracket plugin modules.""" 68 | 69 | plib = obj.get("plugin_library") 70 | if plib is None: 71 | return 72 | 73 | try: 74 | module = _import_module(plib, loaded) 75 | obj["compare"] = getattr(module, "compare", None) 76 | obj["post_match"] = getattr(module, "post_match", None) 77 | obj["validate"] = getattr(module, "validate", None) 78 | obj["highlighting"] = getattr(module, "highlighting", None) 79 | loaded.add(plib) 80 | except Exception: 81 | log("Could not load module %s\n%s" % (plib, str(traceback.format_exc()))) 82 | raise 83 | 84 | 85 | def new_module(name): 86 | """Create a new module.""" 87 | 88 | if sys.version_info < (3, 4): 89 | import imp 90 | return imp.new_module(name) 91 | 92 | import types 93 | return types.ModuleType(name) 94 | 95 | 96 | def _import_module(module_name, loaded=None): 97 | """ 98 | Import the module. 99 | 100 | Import the module and track which modules have been loaded 101 | so we don't load already loaded modules. 102 | """ 103 | 104 | # Pull in built-in and custom plugin directory 105 | if module_name.startswith("bh_modules."): 106 | path_name = join("Packages", "BracketHighlighter", normpath(module_name.replace('.', '/'))) 107 | else: 108 | path_name = join("Packages", normpath(module_name.replace('.', '/'))) 109 | path_name += ".py" 110 | if loaded is not None and module_name in loaded: 111 | module = sys.modules[module_name] 112 | else: 113 | module = new_module(module_name) 114 | sys.modules[module_name] = module 115 | exec( 116 | compile( 117 | sublime.load_resource(sublime_format_path(path_name)), 118 | module_name, 119 | 'exec' 120 | ), 121 | sys.modules[module_name].__dict__ 122 | ) 123 | return module 124 | 125 | 126 | def import_module(module, attribute=None): 127 | """Import module or module attribute.""" 128 | 129 | mod = _import_module(module) 130 | return getattr(mod, attribute) if attribute is not None else mod 131 | 132 | 133 | class BracketPluginRunCommand(sublime_plugin.TextCommand): 134 | """Sublime run command to run BH plugins.""" 135 | 136 | def run(self, edit): 137 | """Run the plugin.""" 138 | 139 | try: 140 | Payload.args["edit"] = edit 141 | Payload.plugin.run(**Payload.args) 142 | Payload.status = True 143 | except Exception: 144 | print("BracketHighlighter: Plugin Run Error:\n%s" % str(traceback.format_exc())) 145 | 146 | 147 | class BracketPlugin(object): 148 | """Class for preparing and running plugins.""" 149 | 150 | def __init__(self, plugin, loaded): 151 | """Load plugin module.""" 152 | 153 | self.enabled = False 154 | self.args = plugin['args'] if ("args" in plugin) else {} 155 | self.plugin = None 156 | if 'command' in plugin: 157 | plib = plugin['command'] 158 | try: 159 | module = _import_module(plib, loaded) 160 | self.plugin = getattr(module, 'plugin')() 161 | loaded.add(plib) 162 | self.enabled = True 163 | except Exception: 164 | print('BracketHighlighter: Load Plugin Error: %s\n%s' % (plugin['command'], traceback.format_exc())) 165 | 166 | def is_enabled(self): 167 | """Check if plugin is enabled.""" 168 | 169 | return self.enabled 170 | 171 | def run_command(self, view, name, left, right, selection): 172 | """Load arguments into plugin and run.""" 173 | 174 | nobracket = False 175 | refresh_match = False 176 | Payload.status = False 177 | Payload.plugin = self.plugin() 178 | setattr(Payload.plugin, "left", left) 179 | setattr(Payload.plugin, "right", right) 180 | setattr(Payload.plugin, "view", view) 181 | setattr(Payload.plugin, "selection", selection) 182 | setattr(Payload.plugin, "nobracket", False) 183 | setattr(Payload.plugin, "refresh_match", False) 184 | self.args["edit"] = None 185 | self.args["name"] = name 186 | Payload.args = self.args 187 | 188 | # Call a `TextCommand` to run the plugin so it can feed in the `Edit` object 189 | view.run_command("bracket_plugin_run") 190 | 191 | if Payload.status: 192 | left = Payload.plugin.left 193 | right = Payload.plugin.right 194 | selection = Payload.plugin.selection 195 | nobracket = Payload.plugin.nobracket 196 | refresh_match = Payload.plugin.refresh_match 197 | Payload.clear() 198 | 199 | return left, right, selection, nobracket, refresh_match 200 | 201 | 202 | class BracketPluginCommand(object): 203 | """Bracket Plugin base class.""" 204 | 205 | def run(self, bracket, content, selection): 206 | """Run the plugin class.""" 207 | 208 | pass 209 | -------------------------------------------------------------------------------- /support.py: -------------------------------------------------------------------------------- 1 | """Support command.""" 2 | import sublime 3 | import sublime_plugin 4 | import textwrap 5 | import webbrowser 6 | import re 7 | 8 | __version__ = "2.33.0" 9 | __pc_name__ = 'BracketHighlighter' 10 | 11 | CSS = ''' 12 | div.bracket-highlighter { padding: 10px; margin: 0; } 13 | .bracket-highlighter h1, .bracket-highlighter h2, .bracket-highlighter h3, 14 | .bracket-highlighter h4, .bracket-highlighter h5, .bracket-highlighter h6 { 15 | {{'.string'|css}} 16 | } 17 | .bracket-highlighter blockquote { {{'.comment'|css}} } 18 | .bracket-highlighter a { text-decoration: none; } 19 | ''' 20 | 21 | frontmatter = { 22 | "markdown_extensions": [ 23 | "markdown.extensions.admonition", 24 | "markdown.extensions.attr_list", 25 | "markdown.extensions.def_list", 26 | "markdown.extensions.nl2br", 27 | # Smart quotes always have corner cases that annoy me, so don't bother with them. 28 | {"markdown.extensions.smarty": {"smart_quotes": False}}, 29 | "pymdownx.betterem", 30 | { 31 | "pymdownx.magiclink": { 32 | "repo_url_shortener": True, 33 | "repo_url_shorthand": True, 34 | "user": "facelessuser", 35 | "repo": "BracketHighlighter" 36 | } 37 | }, 38 | "markdown.extensions.md_in_html", 39 | "pymdownx.keys", 40 | {"pymdownx.escapeall": {"hardbreak": True, "nbsp": True}}, 41 | # Sublime doesn't support superscript, so no ordinal numbers 42 | {"pymdownx.smartsymbols": {"ordinal_numbers": False}} 43 | ] 44 | } 45 | 46 | 47 | def list2string(obj): 48 | """Convert list to string.""" 49 | 50 | return '.'.join([str(x) for x in obj]) 51 | 52 | 53 | def format_version(module, attr, call=False): 54 | """Format the version.""" 55 | 56 | try: 57 | if call: 58 | version = getattr(module, attr)() 59 | else: 60 | version = getattr(module, attr) 61 | except Exception as e: 62 | print(e) 63 | version = 'Version could not be acquired!' 64 | 65 | if not isinstance(version, str): 66 | version = list2string(version) 67 | return version 68 | 69 | 70 | def is_installed_by_package_control(): 71 | """Check if installed by package control.""" 72 | 73 | settings = sublime.load_settings('Package Control.sublime-settings') 74 | return str(__pc_name__ in set(settings.get('installed_packages', []))) 75 | 76 | 77 | class BracketHighlighterSupportInfoCommand(sublime_plugin.ApplicationCommand): 78 | """Support info.""" 79 | 80 | def run(self): 81 | """Run command.""" 82 | 83 | info = {} 84 | 85 | info["platform"] = sublime.platform() 86 | info["version"] = sublime.version() 87 | info["arch"] = sublime.arch() 88 | info["plugin_version"] = __version__ 89 | info["pc_install"] = is_installed_by_package_control() 90 | try: 91 | import mdpopups 92 | info["mdpopups_version"] = format_version(mdpopups, 'version', call=True) 93 | except Exception: 94 | info["mdpopups_version"] = 'Version could not be acquired!' 95 | 96 | try: 97 | import backrefs 98 | info["backrefs_version"] = format_version(backrefs, '__version__') 99 | except Exception: 100 | info["backrefs_version"] = 'Version could not be acquired!' 101 | 102 | msg = textwrap.dedent( 103 | """\ 104 | - ST ver.: %(version)s 105 | - Platform: %(platform)s 106 | - Arch: %(arch)s 107 | - Plugin ver.: %(plugin_version)s 108 | - Install via PC: %(pc_install)s 109 | - mdpopups ver.: %(mdpopups_version)s 110 | - backrefs ver.: %(backrefs_version)s 111 | """ % info 112 | ) 113 | 114 | sublime.message_dialog(msg + '\nInfo has been copied to the clipboard.') 115 | sublime.set_clipboard(msg) 116 | 117 | 118 | class BracketHighlighterOpenSiteCommand(sublime_plugin.ApplicationCommand): 119 | """Open site links.""" 120 | 121 | def run(self, url): 122 | """Open the URL.""" 123 | 124 | webbrowser.open_new_tab(url) 125 | 126 | 127 | class BracketHighlighterDocCommand(sublime_plugin.WindowCommand): 128 | """Open doc page.""" 129 | 130 | re_pkgs = re.compile(r'^Packages') 131 | 132 | def on_navigate(self, href): 133 | """Handle links.""" 134 | 135 | if href.startswith('sub://Packages'): 136 | sublime.run_command('open_file', {"file": self.re_pkgs.sub('${packages}', href[6:])}) 137 | else: 138 | webbrowser.open_new_tab(href) 139 | 140 | def run(self, page): 141 | """Open page.""" 142 | 143 | try: 144 | import mdpopups 145 | has_phantom_support = (mdpopups.version() >= (1, 10, 0)) 146 | fmatter = mdpopups.format_frontmatter(frontmatter) 147 | except Exception: 148 | fmatter = '' 149 | has_phantom_support = False 150 | 151 | if not has_phantom_support: 152 | sublime.run_command('open_file', {"file": page}) 153 | else: 154 | text = sublime.load_resource(page.replace('${packages}', 'Packages')) 155 | view = self.window.new_file() 156 | view.set_name('BracketHighlighter - Quick Start') 157 | view.settings().set('gutter', False) 158 | view.settings().set('word_wrap', False) 159 | if has_phantom_support: 160 | mdpopups.add_phantom( 161 | view, 162 | 'quickstart', 163 | sublime.Region(0), 164 | fmatter + text, 165 | sublime.LAYOUT_INLINE, 166 | css=CSS, 167 | wrapper_class="bracket-highlighter", 168 | on_navigate=self.on_navigate 169 | ) 170 | else: 171 | view.run_command('insert', {"characters": text}) 172 | view.set_read_only(True) 173 | view.set_scratch(True) 174 | 175 | 176 | class BracketHighlighterChangesCommand(sublime_plugin.WindowCommand): 177 | """Changelog command.""" 178 | 179 | def run(self): 180 | """Show the changelog in a new view.""" 181 | try: 182 | import mdpopups 183 | has_phantom_support = (mdpopups.version() >= (1, 10, 0)) 184 | fmatter = mdpopups.format_frontmatter(frontmatter) 185 | except Exception as e: 186 | print(e) 187 | fmatter = '' 188 | has_phantom_support = False 189 | 190 | text = sublime.load_resource('Packages/BracketHighlighter/CHANGES.md') 191 | view = self.window.new_file() 192 | view.set_name('BracketHighlighter - Changelog') 193 | view.settings().set('gutter', False) 194 | view.settings().set('word_wrap', False) 195 | if has_phantom_support: 196 | mdpopups.add_phantom( 197 | view, 198 | 'changelog', 199 | sublime.Region(0), 200 | fmatter + text, 201 | sublime.LAYOUT_INLINE, 202 | wrapper_class="bracket-highlighter", 203 | css=CSS, 204 | on_navigate=self.on_navigate 205 | ) 206 | else: 207 | view.run_command('insert', {"characters": text}) 208 | view.set_read_only(True) 209 | view.set_scratch(True) 210 | 211 | def on_navigate(self, href): 212 | """Open links.""" 213 | webbrowser.open_new_tab(href) 214 | -------------------------------------------------------------------------------- /docs/src/markdown/about/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing & Support 2 | 3 | ## Overview 4 | 5 | Sublime\ Versions | Description 6 | ----------------- | ----------- 7 | ST2 | Supported on a separate branch, but not actively. Any further fixes or enhancements must come from the community. Issues for versions less than ST3 will not be addressed moving forward by me. Pull requests are welcome for back-porting features, enhancements, or fixes to the old branch, but the content of the pull **must** already exist on the main, actively developed branch. I will not allow an older branch to exceed the main branch in regards to functionality. | 8 | ST3 | Fully supported and actively maintained. 9 | 10 | Contribution from the community is encouraged and can be done in a variety of ways: 11 | 12 | - Bug reports. 13 | - Reviewing code. 14 | - Code patches via pull requests. 15 | - Documentation improvements via pull requests. 16 | 17 | /// warning | Bracket Rules are Supported by the Community 18 | The most common requested enhancement for BracketHighlighter is for new rules to add support for \{insert your 19 | favorite language here}. I, like you, am proficient in very specific languages. I probably don't use your favorite 20 | language or there would already be a support for it. I don't have time to learn the nuances of your language. For 21 | these reasons, support for new language brackets **requires** pull requests from the community. 22 | 23 | Though I will not personally implement rules for your favorite language, I am more than willing to offer suggestions 24 | and guidance to help those who may struggle to create rules for their specific language of interest. 25 | /// 26 | 27 | ## Become a Sponsor :octicons-heart-fill-16:{: .heart-throb} 28 | 29 | Open source projects take time and money. Help support the project by becoming a sponsor. You can add your support at 30 | any tier you feel comfortable with. No amount is too little. We also accept one time contributions via PayPal. 31 | 32 | [:octicons-mark-github-16: GitHub Sponsors](https://github.com/sponsors/facelessuser){: .md-button .md-button--primary } 33 | [:fontawesome-brands-paypal: PayPal](https://www.paypal.me/facelessuser){ .md-button} 34 | 35 | ## Bug Reports 36 | 37 | 1. Please **read the documentation** and **search the issue tracker** to try to find the answer to your question 38 | **before** posting an issue. 39 | 40 | 2. When an issue is created, a [template][template] will be shown, please fill out the appropriate sections. If the 41 | template is not followed, the issue will be marked `Invalid` and closed. 42 | 43 | 3. When creating an issue on the repository, please provide as much info as possible. The template will reiterate what 44 | is mentioned here as a reminder: 45 | 46 | - Provide environment information by running `Preferences->Package Settings->BracketHighlighter->Support Info`. The 47 | information will be copied to the clipboard; paste the info in issue. 48 | - Errors in console. 49 | - Detailed description of the problem. 50 | - Examples for reproducing the error. You can post pictures, but if specific text or code is required to reproduce 51 | the issue, please provide the text in a plain text format as well for easy copy/paste. 52 | - Provide links to 3rd party syntax highlighting package you are using if applicable. 53 | 54 | The more info provided the greater the chance someone will take the time to answer, implement, or fix the issue. 55 | 56 | 4. Be prepared to answer questions and provide additional information if required. Issues in which the creator refuses 57 | to respond to follow up questions will be marked as stale and closed. 58 | 59 | ## Reviewing Code 60 | 61 | Take part in reviewing pull requests and/or reviewing direct commits. Make suggestions to improve the code and discuss 62 | solutions to overcome weakness in the algorithm. 63 | 64 | ## Pull Requests 65 | 66 | Pull requests are welcome, and if you plan on contributing directly to the code, there are a couple of things to be 67 | mindful of. 68 | 69 | 1. Please describe the change in as much detail as possible so I can understand what is being added or modified. 70 | 71 | 2. If you are solving a bug that does not already have an issue, please describe the bug in detail and provide info on 72 | how to reproduce if applicable (this is good for me and others to reference later when verifying the issue has been 73 | resolved). 74 | 75 | 3. Please reference and link related open bugs or feature requests in this pull if applicable. 76 | 77 | 4. Make sure you've documented or updated the existing documentation if introducing a new feature or modifying the 78 | behavior of an existing feature that a user needs to be aware of. I will not accept new features or changes to 79 | existing features if you have not provided documentation describing the feature. 80 | 81 | Continuous integration tests on are run on all pull requests and commits via Travis CI. When making a pull request, the 82 | tests will automatically be run, and the request must pass to be accepted. You can (and should) run these tests before 83 | pull requesting. If it is not possible to run these tests locally, they will be run when the pull request is made, but 84 | it is strongly suggested that requesters make an effort to verify before requesting to allow for a quick, smooth merge. 85 | 86 | ### Running Validation Tests 87 | 88 | /// tip | Tip 89 | If you are running Sublime on a OSX or Linux/Unix system, you run all tests by by running the shell script (assuming 90 | you have installed your environment fulfills all requirements below): 91 | 92 | ``` 93 | chmod +x run_tests.sh 94 | ./run_tests.sh 95 | ``` 96 | /// 97 | 98 | There are a couple of dependencies that must be present before running the tests. 99 | 100 | 1. As ST3 is the only current, actively supported version, Python 3.3 must be used to validate the tests. 101 | 102 | 2. Unit tests are run with pytest (@pytest-dev/pytest). You can install pytest via: 103 | 104 | ``` 105 | pip install pytest 106 | ``` 107 | 108 | The tests should be run from the root folder of the plugin by using the following command: 109 | 110 | ``` 111 | pytest . 112 | ``` 113 | 114 | 3. Linting is performed on the entire project with the following modules: 115 | 116 | - @gitlab:pycqa/flake8 117 | - @gitlab:pycqa/flake8-docstrings 118 | - @ebeweber/flake8-mutable 119 | - @gforcada/flake8-builtins 120 | - @gitlab:pycqa/pep8-naming 121 | 122 | These can be installed via: 123 | 124 | ``` 125 | pip install flake8 126 | pip install flake8-docstrings 127 | pip install flake8-mutable 128 | pip install flake8-builtins 129 | pip install pep8-naming 130 | ``` 131 | 132 | Linting is performed with the following command: 133 | 134 | ``` 135 | flake8 . 136 | ``` 137 | 138 | ## Documentation Improvements 139 | 140 | A ton of time has been spent not only creating and supporting this plugin, but also spent making this documentation. If 141 | you feel it is still lacking, show your appreciation for the plugin by helping to improve the documentation. Help with 142 | documentation is always appreciated and can be done via pull requests. There shouldn't be any need to run validation 143 | tests if only updating documentation. 144 | 145 | You don't have to render the docs locally before pull requesting, but if you wish to, I currently use a combination of 146 | @mkdocs/mkdocs, the @squidfunk/mkdocs-material, and @facelessuser/pymdown-extensions to render the docs. You can 147 | preview the docs if you install these two packages. The command for previewing the docs is `mkdocs serve` from the root 148 | directory. You can then view the documents at `localhost:8000`. 149 | -------------------------------------------------------------------------------- /docs/src/markdown/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Package Control 4 | 5 | The recommended way to install BracketHighlighter is via [Package Control][package-control]. Package Control will 6 | install the correct branch on your system and keep it up to date. 7 | 8 | --- 9 | 10 | 1. Ensure Package Control is installed. Instructions are found [here][package-control-install]. 11 | 12 | 2. In Sublime Text, press ++ctrl+shift+p++ (Win, Linux) or ++cmd+shift+p++ (OSX) to bring up the quick panel and start 13 | typing `Package Control: Install Package`. Select the command and it will show a list of installable plugins. 14 | 15 | 3. Start typing `BracketHighlighter`; when you see it, select it. 16 | 17 | 4. Restart to be sure everything is loaded proper. 18 | 19 | 5. Enjoy! 20 | 21 | ## Manual Installation 22 | 23 | /// warning | Warning 24 | This is not the recommended way to install BracketHighlighter for the average user. Installing this way **will 25 | not** get automatically updated. 26 | 27 | If you are forking for a pull request, you should **just** clone BH and run Package Control's `Satisfy Dependency` 28 | command to get all the dependencies. 29 | /// 30 | 31 | For those who want to install BH without package control, here are the steps. It is understood that some people, for 32 | what ever reason, will prefer manual install and may even have legitimate reasons to do so. When going this route, you 33 | will have to keep all the packages updated yourself. 34 | 35 | --- 36 | 37 | 1. Download the latest releases of the following dependencies and unpack or git clone in the `Packages` folder as shown 38 | below: 39 | 40 | - https://bitbucket.org/teddy_beer_maniac/sublime-text-dependency-markupsafe -> `markupsafe` 41 | - https://bitbucket.org/teddy_beer_maniac/sublime-text-dependency-jinja2 -> `python-jinja2` 42 | - https://github.com/packagecontrol/pygments -> `pygments` 43 | - https://github.com/facelessuser/sublime-markdown-popups -> `mdpopups` 44 | - https://github.com/facelessuser/sublime-markdown -> `python-markdown` 45 | - https://github.com/facelessuser/sublime-backrefs -> `backrefs` 46 | 47 | 2. Download and unpack, or git clone, the latest BracketHighlighter release and unpack as `BracketHighlighter`: 48 | 49 | - https://github.com/facelessuser/BracketHighlighter -> BracketHighlighter 50 | 51 | 3. Create a folder under `Packages` called `00-dependencies` and under that folder create a file called 52 | `00-dependencies.py`: 53 | 54 | Copy the following code to `00-dependencies.py` (this code was taken from Package Control): 55 | 56 | ```{.py3 .md-max-height} 57 | import sys 58 | import os 59 | from os.path import dirname 60 | 61 | if os.name == 'nt': 62 | from ctypes import windll, create_unicode_buffer 63 | 64 | import sublime 65 | 66 | 67 | if sys.version_info >= (3,): 68 | def decode(path): 69 | return path 70 | 71 | def encode(path): 72 | return path 73 | 74 | if os.path.basename(__file__) == 'sys_path.py': 75 | pc_package_path = dirname(dirname(__file__)) 76 | # When loaded as a .sublime-package file, the filename ends up being 77 | # Package Control.sublime-package/Package Control.package_control.sys_path 78 | else: 79 | pc_package_path = dirname(__file__) 80 | st_version = u'3' 81 | 82 | else: 83 | def decode(path): 84 | if not isinstance(path, unicode): 85 | path = path.decode(sys.getfilesystemencoding()) 86 | return path 87 | 88 | def encode(path): 89 | if isinstance(path, unicode): 90 | path = path.encode(sys.getfilesystemencoding()) 91 | return path 92 | 93 | pc_package_path = decode(os.getcwd()) 94 | st_version = u'2' 95 | 96 | 97 | st_dir = dirname(dirname(pc_package_path)) 98 | 99 | 100 | def add(path, first=False): 101 | """ 102 | Adds an entry to the beginning of sys.path, working around the fact that 103 | Python 2.6 can't import from non-ASCII paths on Windows. 104 | 105 | :param path: 106 | A unicode string of a folder, zip file or sublime-package file to 107 | add to the path 108 | 109 | :param first: 110 | If the path should be added at the beginning 111 | """ 112 | 113 | if os.name == 'nt': 114 | # Work around unicode path import issue on Windows with Python 2.6 115 | buf = create_unicode_buffer(512) 116 | if windll.kernel32.GetShortPathNameW(path, buf, len(buf)): 117 | path = buf.value 118 | 119 | enc_path = encode(path) 120 | 121 | if os.path.exists(enc_path): 122 | if first: 123 | try: 124 | sys.path.remove(enc_path) 125 | except (ValueError): 126 | pass 127 | sys.path.insert(0, enc_path) 128 | elif enc_path not in sys.path: 129 | sys.path.append(enc_path) 130 | 131 | 132 | def remove(path): 133 | """ 134 | Removes a path from sys.path if it is present 135 | 136 | :param path: 137 | A unicode string of a folder, zip file or sublime-package file 138 | """ 139 | 140 | try: 141 | sys.path.remove(encode(path)) 142 | except (ValueError): 143 | pass 144 | 145 | if os.name == 'nt': 146 | buf = create_unicode_buffer(512) 147 | if windll.kernel32.GetShortPathNameW(path, buf, len(buf)): 148 | path = buf.value 149 | try: 150 | sys.path.remove(encode(path)) 151 | except (ValueError): 152 | pass 153 | 154 | 155 | def generate_dependency_paths(name): 156 | """ 157 | Accepts a dependency name and generates a dict containing the three standard 158 | import paths that are valid for the current machine. 159 | 160 | :param name: 161 | A unicode string name of the dependency 162 | 163 | :return: 164 | A dict with the following keys: 165 | - 'ver' 166 | - 'plat' 167 | - 'arch' 168 | """ 169 | 170 | packages_dir = os.path.join(st_dir, u'Packages') 171 | dependency_dir = os.path.join(packages_dir, name) 172 | 173 | ver = u'st%s' % st_version 174 | plat = sublime.platform() 175 | arch = sublime.arch() 176 | 177 | return { 178 | 'all': os.path.join(dependency_dir, 'all'), 179 | 'ver': os.path.join(dependency_dir, ver), 180 | 'plat': os.path.join(dependency_dir, u'%s_%s' % (ver, plat)), 181 | 'arch': os.path.join(dependency_dir, u'%s_%s_%s' % (ver, plat, arch)) 182 | } 183 | 184 | 185 | def add_dependency(name, first=False): 186 | """ 187 | Accepts a dependency name and automatically adds the appropriate path 188 | to sys.path, if the dependency has a path for the current platform and 189 | architecture. 190 | 191 | :param name: 192 | A unicode string name of the dependency 193 | 194 | :param first: 195 | If the path should be added to the beginning of the list 196 | """ 197 | 198 | dep_paths = generate_dependency_paths(name) 199 | 200 | for path in dep_paths.values(): 201 | if os.path.exists(encode(path)): 202 | add(path, first=first) 203 | 204 | 205 | add_dependency('pygments') 206 | add_dependency('backrefs') 207 | add_dependency('markupsafe') 208 | add_dependency('python-markdown') 209 | add_dependency('python-jinja2') 210 | add_dependency('mdpopups') 211 | 212 | ``` 213 | 214 | 4. Restart and enjoy. 215 | 216 | 217 | ## Git Cloning 218 | 219 | 1. Quit Sublime Text. 220 | 221 | 2. Open a terminal and enter the following. For dependencies, replace the URL with the appropriate URL, and the 222 | appropriate folder to check it out to: 223 | 224 | ``` 225 | cd /path/to/Sublime Text 3/Packages 226 | git clone https://github.com/facelessuser/BracketHighlighter.git BracketHighlighter 227 | ``` 228 | 229 | 3. Restart Sublime Text. 230 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // Debug 3 | { 4 | "caption": "BracketHighlighter: Enable Debug Mode", 5 | "command": "bh_debug", 6 | "args": {"set_value": true} 7 | }, 8 | { 9 | "caption": "BracketHighlighter: Disable Debug Mode", 10 | "command": "bh_debug", 11 | "args": {"set_value": false} 12 | }, 13 | { 14 | "caption": "BracketHighlighter: (Debug) Filter Rules by Key", 15 | "command": "bh_debug_rule", 16 | "args": {"filter_key": true} 17 | }, 18 | { 19 | "caption": "BracketHighlighter: (Debug) Show Merged Rules", 20 | "command": "bh_debug_rule" 21 | }, 22 | // Toggle Global Enable 23 | { 24 | "caption": "BracketHighlighter: Toggle Global Enable", 25 | "command": "bh_toggle_enable" 26 | }, 27 | // Search to end of file for bracket 28 | { 29 | "caption": "BracketHighlighter: Match Brackets (ignore threshold)", 30 | "command": "bh_async_key", 31 | "args": {"lines" : true} 32 | }, 33 | // Remove brackets 34 | { 35 | "caption": "BracketHighlighter: Remove Brackets", 36 | "command": "bh_remove_brackets" 37 | }, 38 | // Go to left bracket 39 | { 40 | "caption": "BracketHighlighter: Jump to Left Bracket", 41 | "command": "bh_async_key", 42 | "args": 43 | { 44 | "no_outside_adj": null, 45 | "no_block_mode": null, 46 | "lines" : true, 47 | "plugin": 48 | { 49 | "type": ["__all__"], 50 | "command": "bh_modules.bracketselect", 51 | "args": {"select": "left"} 52 | } 53 | } 54 | }, 55 | // Go to right bracket 56 | { 57 | "caption": "BracketHighlighter: Jump to Right Bracket", 58 | "command": "bh_async_key", 59 | "args": 60 | { 61 | "no_outside_adj": null, 62 | "no_block_mode": null, 63 | "lines" : true, 64 | "plugin": 65 | { 66 | "type": ["__all__"], 67 | "command": "bh_modules.bracketselect", 68 | "args": {"select": "right"} 69 | } 70 | } 71 | }, 72 | // Select text between brackets 73 | { 74 | "caption": "BracketHighlighter: Select Bracket Content", 75 | "command": "bh_async_key", 76 | "args": 77 | { 78 | "no_outside_adj": null, 79 | "lines" : true, 80 | "plugin": 81 | { 82 | "type": ["__all__"], 83 | "command": "bh_modules.bracketselect" 84 | } 85 | } 86 | }, 87 | // Select text including brackets 88 | { 89 | "caption": "BracketHighlighter: Select Bracket Content with Brackets", 90 | "command": "bh_async_key", 91 | "args": 92 | { 93 | "no_outside_adj": null, 94 | "lines" : true, 95 | "plugin": 96 | { 97 | "type": ["__all__"], 98 | "command": "bh_modules.bracketselect", 99 | "args": {"always_include_brackets": true} 100 | } 101 | } 102 | }, 103 | // Fold contents between brackets 104 | { 105 | "caption": "BracketHighlighter: Fold Bracket Content", 106 | "command": "bh_async_key", 107 | "args": 108 | { 109 | "plugin": { 110 | "type": ["__all__"], 111 | "command" : "bh_modules.foldbracket" 112 | } 113 | } 114 | }, 115 | { "caption": "-" }, 116 | // Toggle between string and regex escape mode for string brackets 117 | { 118 | "caption": "BracketHighlighter: Toggle String Bracket Escape Mode", 119 | "command": "bh_toggle_string_escape_mode" 120 | }, 121 | // Toggle high visibility mode 122 | { 123 | "caption": "BracketHighlighter: Toggle High Visibility Mode", 124 | "command": "bh_toggle_high_visibility" 125 | }, 126 | { "caption": "-" }, 127 | // Select tag name of HTML/XML tag (both opening name and closing) 128 | { 129 | "caption": "BracketHighlighter: Select Tag Name (closing and opening)", 130 | "command": "bh_async_key", 131 | "args": 132 | { 133 | "plugin": 134 | { 135 | "type": ["cfml", "html", "angle"], 136 | "command": "bh_modules.tagnameselect" 137 | } 138 | } 139 | }, 140 | // Select the attribute to the right of the cursor (will wrap inside the tag) 141 | { 142 | "caption": "BracketHighlighter: Select Next Attribute (right)", 143 | "command": "bh_async_key", 144 | "args": 145 | { 146 | "plugin": 147 | { 148 | "type": ["cfml", "html", "angle"], 149 | "command": "bh_modules.tagattrselect", 150 | "args": {"direction": "right"} 151 | } 152 | } 153 | }, 154 | // Select the attribute to the left of the cursor (will wrap inside the tag) 155 | { 156 | "caption": "BracketHighlighter: Select Next Attribute (left)", 157 | "command": "bh_async_key", 158 | "args": 159 | { 160 | "plugin": 161 | { 162 | "type": ["cfml", "html", "angle"], 163 | "command": "bh_modules.tagattrselect", 164 | "args": {"direction": "left"} 165 | } 166 | } 167 | }, 168 | // Convert single quote string to double quoted string and vice versa 169 | // Will handle escaping or unescaping quotes within the string 170 | { 171 | "caption": "BracketHighlighter: Swap Quotes", 172 | "command": "bh_async_key", 173 | "args": 174 | { 175 | "lines" : true, 176 | "plugin": 177 | { 178 | "type": ["single_quote", "double_quote", "py_single_quote", "py_double_quote"], 179 | "command": "bh_modules.swapquotes" 180 | } 181 | } 182 | }, 183 | // Swap Brackets 184 | { 185 | "caption": "BracketHighlighter: Swap Brackets", 186 | "command": "swap_brackets", 187 | "args": {"async": true} 188 | }, 189 | // Surround selection with brackets from quick panel 190 | { 191 | "caption": "BracketHighlighter: Wrap Selections with Brackets", 192 | "command": "wrap_brackets" 193 | }, 194 | { 195 | "caption": "BracketHighlighter: Find Matching Offscreen Bracket", 196 | "command": "bh_offscreen_popup" 197 | }, 198 | { 199 | "caption": "BracketHighlighter: Settings", 200 | "command": "edit_settings", 201 | "args": { 202 | "base_file": "${packages}/BracketHighlighter/bh_core.sublime-settings", 203 | "default": "{\n$0\n}\n" 204 | } 205 | }, 206 | { 207 | "caption": "BracketHighlighter: Tag Settings", 208 | "command": "edit_settings", 209 | "args": { 210 | "base_file": "${packages}/BracketHighlighter/bh_tag.sublime-settings", 211 | "default": "{\n$0\n}\n" 212 | } 213 | }, 214 | { 215 | "caption": "BracketHighlighter: Wrap Settings", 216 | "command": "edit_settings", 217 | "args": { 218 | "base_file": "${packages}/BracketHighlighter/bh_wrapping.sublime-settings", 219 | "default": "{\n$0\n}\n" 220 | } 221 | }, 222 | { 223 | "caption": "BracketHighlighter: Swap Settings", 224 | "command": "edit_settings", 225 | "args": { 226 | "base_file": "${packages}/BracketHighlighter/bh_swapping.sublime-settings", 227 | "default": "{\n$0\n}\n" 228 | } 229 | }, 230 | { 231 | "caption": "BracketHighlighter: Documentation", 232 | "command": "bracket_highlighter_open_site", 233 | "args": { 234 | "url": "http://facelessuser.github.io/BracketHighlighter/" 235 | } 236 | }, 237 | { 238 | "caption": "BracketHighlighter: Quick Start Guide", 239 | "command": "bracket_highlighter_doc", 240 | "args": { 241 | "page": "${packages}/BracketHighlighter/quickstart.md" 242 | } 243 | } 244 | ] 245 | -------------------------------------------------------------------------------- /tests/validate_json_format.py: -------------------------------------------------------------------------------- 1 | """ 2 | Validate JSON format. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012-2015 Isaac Muse 6 | """ 7 | import re 8 | import codecs 9 | import json 10 | 11 | RE_LINE_PRESERVE = re.compile(r"\r?\n", re.MULTILINE) 12 | RE_COMMENT = re.compile( 13 | r'''(?x) 14 | (?P 15 | /\*[^*]*\*+(?:[^/*][^*]*\*+)*/ # multi-line comments 16 | | [ \t]*//(?:[^\r\n])* # single line comments 17 | ) 18 | | (?P 19 | "(?:\\.|[^"\\])*" # double quotes 20 | | .[^/"']* # everything else 21 | ) 22 | ''', 23 | re.DOTALL 24 | ) 25 | RE_TRAILING_COMMA = re.compile( 26 | r'''(?x) 27 | ( 28 | (?P 29 | , # trailing comma 30 | (?P[\s\r\n]*) # white space 31 | (?P\]) # bracket 32 | ) 33 | | (?P 34 | , # trailing comma 35 | (?P[\s\r\n]*) # white space 36 | (?P\}) # bracket 37 | ) 38 | ) 39 | | (?P 40 | "(?:\\.|[^"\\])*" # double quoted string 41 | | .[^,"']* # everything else 42 | ) 43 | ''', 44 | re.DOTALL 45 | ) 46 | RE_LINE_INDENT_TAB = re.compile(r'^(?:(\t+)?(?:(/\*)|[^ \t\r\n])[^\r\n]*)?\r?\n$') 47 | RE_LINE_INDENT_SPACE = re.compile(r'^(?:((?: {4})+)?(?:(/\*)|[^ \t\r\n])[^\r\n]*)?\r?\n$') 48 | RE_TRAILING_SPACES = re.compile(r'^.*?[ \t]+\r?\n?$') 49 | RE_COMMENT_END = re.compile(r'\*/') 50 | PATTERN_COMMENT_INDENT_SPACE = r'^(%s *?[^\t\r\n][^\r\n]*)?\r?\n$' 51 | PATTERN_COMMENT_INDENT_TAB = r'^(%s[ \t]*[^ \t\r\n][^\r\n]*)?\r?\n$' 52 | 53 | 54 | E_MALFORMED = "E0" 55 | E_COMMENTS = "E1" 56 | E_COMMA = "E2" 57 | W_NL_START = "W1" 58 | W_NL_END = "W2" 59 | W_INDENT = "W3" 60 | W_TRAILING_SPACE = "W4" 61 | W_COMMENT_INDENT = "W5" 62 | 63 | 64 | VIOLATION_MSG = { 65 | E_MALFORMED: 'JSON content is malformed.', 66 | E_COMMENTS: 'Comments are not part of the JSON spec.', 67 | E_COMMA: 'Dangling comma found.', 68 | W_NL_START: 'Unnecessary newlines at the start of file.', 69 | W_NL_END: 'Missing a new line at the end of the file.', 70 | W_INDENT: 'Indentation Error.', 71 | W_TRAILING_SPACE: 'Trailing whitespace.', 72 | W_COMMENT_INDENT: 'Comment Indentation Error.' 73 | } 74 | 75 | 76 | class CheckJsonFormat(object): 77 | """ 78 | Test JSON for format irregularities. 79 | 80 | - Trailing spaces. 81 | - Inconsistent indentation. 82 | - New lines at end of file. 83 | - Unnecessary newlines at start of file. 84 | - Trailing commas. 85 | - Malformed JSON. 86 | """ 87 | 88 | def __init__(self, use_tabs=False, allow_comments=False): 89 | """Setup the settings.""" 90 | 91 | self.use_tabs = use_tabs 92 | self.allow_comments = allow_comments 93 | self.fail = False 94 | 95 | def index_lines(self, text): 96 | """Index the char range of each line.""" 97 | 98 | self.line_range = [] 99 | count = 1 100 | last = 0 101 | for m in re.finditer('\n', text): 102 | self.line_range.append((last, m.end(0) - 1, count)) 103 | last = m.end(0) 104 | count += 1 105 | 106 | def get_line(self, pt): 107 | """Get the line from char index.""" 108 | 109 | line = None 110 | for r in self.line_range: 111 | if pt >= r[0] and pt <= r[1]: 112 | line = r[2] 113 | break 114 | return line 115 | 116 | def check_comments(self, text): 117 | """ 118 | Check for JavaScript comments. 119 | 120 | Log them and strip them out so we can continue. 121 | """ 122 | 123 | def remove_comments(group): 124 | return ''.join([x[0] for x in RE_LINE_PRESERVE.findall(group)]) 125 | 126 | def evaluate(m): 127 | text = '' 128 | g = m.groupdict() 129 | if g["code"] is None: 130 | if not self.allow_comments: 131 | self.log_failure(E_COMMENTS, self.get_line(m.start(0))) 132 | text = remove_comments(g["comments"]) 133 | else: 134 | text = g["code"] 135 | return text 136 | 137 | content = ''.join(map(lambda m: evaluate(m), RE_COMMENT.finditer(text))) 138 | return content 139 | 140 | def check_dangling_commas(self, text): 141 | """ 142 | Check for dangling commas. 143 | 144 | Log them and strip them out so we can continue. 145 | """ 146 | 147 | def check_comma(g, m, line): 148 | # ,] -> ] or ,} -> } 149 | self.log_failure(E_COMMA, line) 150 | if g["square_comma"] is not None: 151 | return g["square_ws"] + g["square_bracket"] 152 | else: 153 | return g["curly_ws"] + g["curly_bracket"] 154 | 155 | def evaluate(m): 156 | g = m.groupdict() 157 | return check_comma(g, m, self.get_line(m.start(0))) if g["code"] is None else g["code"] 158 | 159 | return ''.join(map(lambda m: evaluate(m), RE_TRAILING_COMMA.finditer(text))) 160 | 161 | def log_failure(self, code, line=None): 162 | """ 163 | Log failure. 164 | 165 | Log failure code, line number (if available) and message. 166 | """ 167 | 168 | if line: 169 | print("%s: Line %d - %s" % (code, line, VIOLATION_MSG[code])) 170 | else: 171 | print("%s: %s" % (code, VIOLATION_MSG[code])) 172 | self.fail = True 173 | 174 | def check_format(self, file_name): 175 | """Initiate the check.""" 176 | 177 | self.fail = False 178 | comment_align = None 179 | with codecs.open(file_name, encoding='utf-8') as f: 180 | count = 1 181 | for line in f: 182 | indent_match = (RE_LINE_INDENT_TAB if self.use_tabs else RE_LINE_INDENT_SPACE).match(line) 183 | end_comment = ( 184 | (comment_align is not None or (indent_match and indent_match.group(2))) and 185 | RE_COMMENT_END.search(line) 186 | ) 187 | # Don't allow empty lines at file start. 188 | if count == 1 and line.strip() == '': 189 | self.log_failure(W_NL_START, count) 190 | # Line must end in new line 191 | if not line.endswith('\n'): 192 | self.log_failure(W_NL_END, count) 193 | # Trailing spaces 194 | if RE_TRAILING_SPACES.match(line): 195 | self.log_failure(W_TRAILING_SPACE, count) 196 | # Handle block comment content indentation 197 | if comment_align is not None: 198 | if comment_align.match(line) is None: 199 | self.log_failure(W_COMMENT_INDENT, count) 200 | if end_comment: 201 | comment_align = None 202 | # Handle general indentation 203 | elif indent_match is None: 204 | self.log_failure(W_INDENT, count) 205 | # Enter into block comment 206 | elif comment_align is None and indent_match.group(2): 207 | alignment = indent_match.group(1) if indent_match.group(1) is not None else "" 208 | if not end_comment: 209 | comment_align = re.compile( 210 | (PATTERN_COMMENT_INDENT_TAB if self.use_tabs else PATTERN_COMMENT_INDENT_SPACE) % alignment 211 | ) 212 | count += 1 213 | f.seek(0) 214 | text = f.read() 215 | 216 | self.index_lines(text) 217 | text = self.check_comments(text) 218 | self.index_lines(text) 219 | text = self.check_dangling_commas(text) 220 | try: 221 | json.loads(text) 222 | except Exception as e: 223 | self.log_failure(E_MALFORMED) 224 | print(e) 225 | return self.fail 226 | 227 | 228 | if __name__ == "__main__": 229 | import sys 230 | cjf = CheckJsonFormat(False, True) 231 | cjf.check_format(sys.argv[1]) 232 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # BracketHighlighter 2 | 3 | ## 2.33.0 4 | 5 | - **NEW**: Release special branch for Sublime Text 4201+. 6 | 7 | ## 2.32.4 8 | 9 | - **FIX**: Fixes for Sublime beta using Python 3.13. 10 | 11 | ## 2.32.3 12 | 13 | - **FIX**: Fix example keymap for selection. 14 | - **FIX**: Use typing dependency. 15 | 16 | ## 2.32.2 17 | 18 | - **FIX**: Fix bracket highlighting in Bash due to recent changes in syntax language. 19 | 20 | ## 2.32.1 21 | 22 | - **FIX**: Remove verbose parameter from named tubple which causes issues with Python 3.8. 23 | 24 | ## 2.32.0 25 | 26 | - **NEW**: Opt in to Python 3.8. 27 | - **FIX**: Fix issues with "SINUMERIK840D" language. 28 | 29 | ## 2.31.5 30 | 31 | - **FIX**: Fix endless Ruby method case. 32 | 33 | ## 2.31.4 34 | 35 | - **FIX**: Fix PHP arrow case that could break bracket highlighting. 36 | 37 | ## 2.31.3 38 | 39 | - **FIX**: Fix some Bash cases. 40 | 41 | ## 2.31.2 42 | 43 | - **FIX**: Fix performance with content highlight bar. 44 | 45 | ## 2.31.1 46 | 47 | - **FIX**: Fix issue with CMFL bracket matching. 48 | 49 | ## 2.31 50 | 51 | - **NEW**: Add support for Ruby endless methods. 52 | - **NEW**: `validate` plugin method has been updated to accept a View, but will fallback to the legacy approach 53 | for backwards compatibility. 54 | 55 | ## 2.30.1 56 | 57 | - **FIX**: Fix PHP angle matching rule. 58 | 59 | ## 2.30.0 60 | 61 | - **NEW**: When defining key bindings `type` is now defaulted to `['__all__']` if not set. 62 | - **FIX**: Ensure Jinja2 support for works for the Jinja2 package (support existed for some older package). 63 | 64 | ## 2.29.4 65 | 66 | - **FIX**: Fix for "Ruby on Rails" embedded in HTML 4130+. 67 | 68 | ## 2.29.3 69 | 70 | - **FIX**: Fix "Ruby on Rails" for Sublime build 4130+. 71 | - **FIX**: Quotes matching for SQL string in PHP. 72 | 73 | ## 2.29.2 74 | 75 | - **FIX**: Adjustments for latest syntax changes in default JavaScript and HTML in Sublime Text. Fixes issues with 76 | JavaScript angle brackets. 77 | - **FIX**: Elixir end block detection. 78 | 79 | ## 2.29.1 80 | 81 | - **FIX**: PHP bracket matching issue. 82 | - **FIX**: Fix Lua keyword issue. 83 | 84 | ## 2.29.0 85 | 86 | - **NEW**: Enhance bracket content select feature with `extend` parameter. 87 | - **FIX**: Bash switch case highlighting on ST3. 88 | - **FIX**: Fix custom popup color handling. 89 | 90 | ## 2.28.1 91 | 92 | - **FIX**: Handle HTML attributes even when there are no spaces between them. 93 | 94 | ## 2.28.0 95 | 96 | - **NEW**: Rename `language_filter` options `whitelist` and `blacklist` to `allowlist` and `blocklist` respectively. 97 | - **NEW**: Add global option `gutter_icons` to control enabling or disabling icons. 98 | 99 | ## 2.27.10 100 | 101 | - **FIX**: Handle certain regular expression compilation failures in a more graceful way. 102 | 103 | ## 2.27.9 104 | 105 | - **FIX**: Remove old clone workaround as this issue will be fixed upstream in Sublime Text 4 builds. 106 | 107 | ## 2.27.8 108 | 109 | - **FIX**: Content align bug. 110 | 111 | ## 2.27.7 112 | 113 | - **FIX**: Update support to include OCaml comment support. 114 | - **FIX**: Fix avoiding round brackets in shell case statements. 115 | - **FIX**: Thread adjustments that allow BracketHighlighter to go completely idle when Sublime Text is idle. 116 | - **FIX**: Fix Ruby interpolated strings. 117 | - **FIX**: Fix optional tags for `option` and `optgroup`. 118 | 119 | ## 2.27.6 120 | 121 | - **FIX**: Fix issue where HTML style attribute quotes where not highlighted due to syntax definition changes. 122 | - **FIX**: Add support for `@` in HTML attributes. 123 | 124 | ## 2.27.5 125 | 126 | - **FIX**: Fix issue where bracket context code blocks in popups sometimes are recognized as Jinja2 template 127 | variables. 128 | - **FIX**: Fix internal clone view cleanup. 129 | - **FIX**: Fix bad clone reference. 130 | - **FIX**: `on_hover` should not occur if `bracket_highlighter.ignore` is set in the view. 131 | 132 | ## 2.27.4 133 | 134 | - **FIX**: Avoid targeting common `HERDOC` syntax with angle brackets. #482 135 | 136 | ## 2.27.3 137 | 138 | - **FIX**: Selecting "no threshold" search from popup quickly reverts back to unmatched. 139 | - **FIX**: Backtick string support extended to JavaScript. 140 | 141 | ## 2.27.2 142 | 143 | - **FIX**: `C#` interpolated strings !468. 144 | - **FIX**: Fix C/C++ preprocessor highlighting !474. 145 | - **FIX**: Only highlight the keyword in C/C++ preprocessors af22600cd23bd3c15a1a0f6fc54041e6d96b3dd3. 146 | 147 | ## 2.27.1 148 | 149 | - **FIX**: Fix Lua loops by avoiding `while` and `from` and just highlighting `do` #466. 150 | 151 | ## 2.27.0 152 | 153 | - **NEW**: Add option to always show the bracket popup on bracket hover #457. 154 | - **FIX**: Fix clone views not properly supported #454. 155 | - **FIX**: Improvements to Ruby conditional matching #452. 156 | 157 | ## 2.26.0 158 | 159 | - **NEW**: Added new configuration `user_bracket_styles` to allow a user to override specific rules or just part of a 160 | specific rule instead of copying all of `bracket_styles` #448. 161 | - **NEW**: Add colorization with region-ish scopes for Sublime builds 3148+ #448. 162 | - **FIX**: Ruby issue with conditionals immediately followed after return keyword #425. 163 | - **FIX**: PHP issue for arrows (`$var->prop`) #446. 164 | 165 | ## 2.25.2 166 | 167 | - **FIX**: Update tag attribute pattern. 168 | - **FIX**: Add SVG self closing tags. 169 | - **FIX**: Temporarily use `thin_underline` style to mitigate issue #443. 170 | 171 | ## 2.25.1 172 | 173 | - **FIX**: Update dependencies. 174 | 175 | ## 2.25.0 176 | 177 | - **FIX**: Quick start image links. 178 | - **FIX**: Allow Markdown related brackets to work in Markdown Extended. 179 | - **FIX**: Allow `HTML (Jinja2)` to work in HTML. 180 | - **NEW**: Add Markdown `` ` `` to swap and wrap. 181 | - **NEW**: Add commonly used commands to the command panel (documents and settings) #419. 182 | 183 | ## 2.24.2 184 | 185 | - **FIX**: Avoid things like `->` in PHP due to new Sublime default syntax changes #417. 186 | - **FIX**: Add support for Python f-strings. 187 | 188 | ## 2.24.1 189 | 190 | - **FIX**: Random regions failure. 191 | - **FIX**: Lua keyword match at beginning of line. 192 | 193 | ## 2.24.0 194 | 195 | - **NEW**: Popup/Phantom support limited to 3124+ moving forward to prepare for `mdpopups` 2.0 that will drop legacy 196 | support for old, early implementation of popups and phantoms. 197 | - **NEW**: No longer try and force dependency updates. Leave it up to Package Control (whether they do it or not). 198 | - **NEW**: CSS adjustments to popups. 199 | - **FIX**: Fix tag matching corner case #409. 200 | 201 | ## 2.23.3 202 | 203 | - **FIX**: Fix error `ImportError: No module named 'yaml'` #400. 204 | 205 | ## 2.23.2 206 | 207 | - **FIX**: Add backtick quote support for ruby and shell script syntaxes d884e8ab7aa69477c1af5d29cef24589efaf2b8e. 208 | - **FIX**: Fix console noise on global disable #397. 209 | 210 | ## 2.23.1 211 | 212 | - **FIX**: Rule position - zero is a valid position #387. 213 | - **FIX**: Protect against race condition due to Sublime bug #390. 214 | 215 | ## 2.23.0 216 | 217 | - **NEW**: Add links in menu to documentation and issues. 218 | - **NEW**: Provide new local quickstart guide from the menu. 219 | - **NEW**: Breaking change to `bh_tag.sublime-settings`. `tag_mode` is now an ordered list of dictionaries. 220 | `self_closing_patterns` and `single_tag_patterns` and replaced with `optional_tag_patterns`, 221 | `void_tag_patterns`, and `self_closing_tag_patterns`. 222 | - **NEW**: Add new `first_line` rule for determining tag mode. 223 | - **NEW**: New XML tag mode and better XHTML mode. 224 | - **NEW**: Better special tag logic which handles optional tags, void tags, and self closing tags better. #384 225 | 226 | ## 2.22.1 227 | 228 | - **FIX**: Fix changelog links 229 | 230 | ## 2.22.0 231 | 232 | - **NEW**: Manual command to show offscreen bracket popup. Can be invoked when cursor is anywhere between target 233 | bracket #378. 234 | - **NEW**: When selecting the "Match brackets without threshold" link on the unmatched bracket popup, reshow the 235 | offscreen popup. 236 | - **NEW**: Add support for "SINUMERIK840D" language #379. 237 | 238 | ## 2.21.6 239 | 240 | - **FIX**: Fix PHP conditional #366. 241 | - **FIX**: No line wrapping in code snippets in popups. 242 | 243 | ## 2.21.5 244 | 245 | - **FIX**: Fix a break caused by 2.21.4 #364. 246 | - **FIX**: Fix CSS in changelog 247 | 248 | ## 2.21.4 249 | 250 | - **FIX**: Changelog command now works for older ST3 versions. 251 | 252 | ## 2.21.3 253 | 254 | - **FIX**: Don't fail if mdpopups was not installed on old Sublime version. 255 | 256 | ## 2.21.2 257 | 258 | - **FIX**: Fix changelog typo :). 259 | 260 | ## 2.21.1 261 | 262 | - **NEW**: Message to not freak people out :). 263 | 264 | ## 2.21.0 265 | 266 | - **NEW**: Require mdpopups 1.9.0. 267 | - **NEW**: New changelog command. 268 | - **NEW**: Add support for new LaTeX syntax. 269 | -------------------------------------------------------------------------------- /bh_popup.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | 7 | Handles popups. 8 | """ 9 | import sublime 10 | import re 11 | import textwrap 12 | import traceback 13 | from .bh_logging import log 14 | 15 | HOVER_SUPPORT = int(sublime.version()) >= 3124 16 | WRAPPER_CLASS = "bracket-highlighter" 17 | 18 | CSS = ''' 19 | {%- if var.mdpopups_version >= (2, 0, 0) %} 20 | div.bracket-highlighter { padding: 0.5rem; margin: 0; } 21 | {%- else %} 22 | div.bracket-highlighter { padding: 0; margin: 0; } 23 | {%- endif %} 24 | ''' 25 | MATCH_ERR = ''' 26 | \x02{%%- if plugin.mdpopups_version >= (2, 0, 0) %%}\x03 27 | !!! panel-error "Matching bracket could not be found!" 28 | 29 | - There *might* be no match. 30 | - Brackets *might* be nested poorly --> `([)(])` 31 | - Matching bracket *might* be beyond the search threshold. 32 | A match done without the threshold *might* find it. 33 | 34 | [(Match brackets without threshold)](%(pt)s) 35 | \x02{%%- else %%}\x03 36 | ### Matching bracket could not be found! {: .error} 37 | 38 | - There *might* be no match. 39 | - Brackets *might* be nested poorly --> `([)(])` 40 | - Matching bracket *might* be beyond the search threshold. 41 | A match done without the threshold *might* find it. 42 | [(Match brackets without threshold)](%(pt)s) 43 | \x02{%%- endif %%}\x03 44 | ''' 45 | 46 | template_options = { 47 | "block_start_string": "\x02{%", 48 | "block_end_string": "%}\x03", 49 | "variable_start_string": "\x02{{", 50 | "variable_end_string": "}}\x03", 51 | "comment_start_string": "\x02{#", 52 | "comment_end_string": "#}\x03" 53 | } 54 | 55 | if HOVER_SUPPORT: 56 | import mdpopups 57 | 58 | 59 | class BhOffscreenPopup(object): 60 | """Handle offscreen popups.""" 61 | 62 | popup_view = None 63 | 64 | def on_navigate(self, href): 65 | """Navigate to code position.""" 66 | if HOVER_SUPPORT: 67 | try: 68 | pt = int(href) 69 | self.popup_view.sel().clear() 70 | self.popup_view.sel().add(sublime.Region(pt)) 71 | self.popup_view.show(pt) 72 | mdpopups.hide_popup(self.popup_view) 73 | except Exception: 74 | log("Problem handling popup event:\n%s" % str(traceback.format_exc())) 75 | 76 | def on_navigate_unmatched(self, href): 77 | """Handle unmatched click.""" 78 | if HOVER_SUPPORT: 79 | pt = int(href) 80 | mdpopups.hide_popup(self.popup_view) 81 | self.popup_view.run_command("bh_offscreen_popup", {"point": pt, "no_threshold": True}) 82 | 83 | def show_unmatched_popup(self, view, point): 84 | """Show unmatched popup.""" 85 | 86 | if HOVER_SUPPORT: 87 | self.popup_view = view 88 | mdpopups.show_popup( 89 | view, 90 | textwrap.dedent(MATCH_ERR % {"pt": str(point)}), 91 | wrapper_class=WRAPPER_CLASS, 92 | flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, 93 | css=CSS, 94 | max_width=800, 95 | max_height=800, 96 | location=point, 97 | on_navigate=self.on_navigate_unmatched, 98 | template_vars={'mdpopups_version': mdpopups.version()}, 99 | template_env_options=template_options 100 | ) 101 | 102 | def is_bracket_visible(self, view, region): 103 | """ 104 | Check if bracket is visible. 105 | 106 | Check if first point of bracket is visible or last point is visible. 107 | In short, is any part visible? 108 | 109 | ``` 110 | (xa,ya)--------------------(xb,ya) 111 | | | 112 | | | 113 | | (pxa,pya){(pxb,pyb) | 114 | | | 115 | | | 116 | (xa,yb)------------------------ 117 | ``` 118 | """ 119 | 120 | # Always report that bracket is not visible if we always want to show the popup 121 | if sublime.load_settings("bh_core.sublime-settings").get('show_bracket_popup_always', False): 122 | return False 123 | 124 | xa, ya = view.viewport_position() 125 | w, h = view.viewport_extent() 126 | xb, yb = xa + w, ya + h 127 | pxa, pya = view.text_to_layout(region[0]) 128 | pxb, pyb = view.text_to_layout(region[1]) 129 | 130 | return (xa <= pxa < xb and ya <= pya < yb) or (xa <= pxb < xb and ya <= pyb < yb) 131 | 132 | def get_context_line(self, view, row, col_start, col_end, tab_size): 133 | """Get line context.""" 134 | 135 | line = view.line(view.text_point(row, 0)) 136 | c0 = view.rowcol(line.begin())[1] 137 | c1 = view.rowcol(line.end())[1] 138 | if c0 < col_start: 139 | c0 = col_start 140 | if c0 > c1: 141 | c0 = c1 142 | diff = c1 - c0 143 | if diff > 120: 144 | c1 -= diff - 120 145 | 146 | return self.escape_code( 147 | view.substr( 148 | sublime.Region( 149 | view.text_point(row, c0), 150 | view.text_point(row, c1) 151 | ) 152 | ), 153 | tab_size 154 | ) 155 | 156 | def get_multiline_context(self, view, code, row, col_start, col_end, tab_size, line_context): 157 | """Get multi-line context for vertical offscreen brackets.""" 158 | 159 | last_row = view.rowcol(view.size())[0] 160 | 161 | lines = [code] 162 | 163 | offset = 1 164 | while offset <= line_context: 165 | current_row = row - offset 166 | if current_row >= 0: 167 | lines.insert(0, self.get_context_line(view, current_row, col_start, col_end, tab_size)) 168 | else: 169 | break 170 | offset += 1 171 | last_offset = offset 172 | 173 | offset = 1 174 | while offset <= line_context or (len(lines) != (line_context * 2) + 1): 175 | current_row = row + offset 176 | if current_row <= last_row: 177 | lines.append(self.get_context_line(view, current_row, col_start, col_end, tab_size)) 178 | else: 179 | break 180 | offset += 1 181 | 182 | offset = last_offset 183 | while len(lines) < ((line_context * 2) + 1): 184 | current_row = row - offset 185 | if current_row >= 0: 186 | lines.insert(0, self.get_context_line(view, current_row, col_start, col_end, tab_size)) 187 | else: 188 | break 189 | offset += 1 190 | 191 | return textwrap.dedent('\n'.join(lines)).replace('\n', '
') 192 | 193 | def escape_code(self, text, tab_size): 194 | """Format text to HTML.""" 195 | 196 | encode_table = { 197 | '&': '&', 198 | '>': '>', 199 | '<': '<', 200 | '\t': ' ' * tab_size, 201 | '\n': '', 202 | ' ': ' ' 203 | } 204 | 205 | return ''.join( 206 | encode_table.get(c, c) for c in text 207 | ) 208 | 209 | def show_popup_between(self, view, point, region, region2, icon): 210 | """Show popup between.""" 211 | if HOVER_SUPPORT: 212 | markup = '' 213 | if not self.is_bracket_visible(view, region): 214 | markup += self.get_markup(view, point, region, icon) 215 | if not self.is_bracket_visible(view, region2): 216 | markup += '\n\n' 217 | markup += self.get_markup(view, point, region2, icon) 218 | if markup: 219 | self.popup_view = view 220 | mdpopups.show_popup( 221 | view, 222 | markup, 223 | wrapper_class=WRAPPER_CLASS, 224 | css=CSS, 225 | flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, 226 | max_width=800, 227 | location=point, 228 | on_navigate=self.on_navigate, 229 | template_vars={'mdpopups_version': mdpopups.version()}, 230 | template_env_options=template_options 231 | ) 232 | else: 233 | self.show_unmatched_popup(view, point) 234 | 235 | def get_markup(self, view, point, region, icon): 236 | """Get markup.""" 237 | 238 | settings = sublime.load_settings('bh_core.sublime-settings') 239 | tab_size = view.settings().get('tab_size', 4) 240 | 241 | # Get highlight colors 242 | color = None 243 | if icon is not None: 244 | color = mdpopups.scope2style(view, icon[1]).get('color') 245 | if color is None or bool(settings.get('use_custom_popup_bracket_emphasis', False)): 246 | bracket_em = settings.get('popup_bracket_emphasis', '#ff0000') 247 | if not bracket_em.startswith('#'): 248 | bracket_em = mdpopups.scope2style(view, bracket_em).get('color') 249 | else: 250 | bracket_em = color 251 | 252 | # Get positions of bracket extents on the line 253 | row, col = view.rowcol(region[0]) 254 | col2 = view.rowcol(region[1])[1] 255 | 256 | # Calculate how far before and after bracket we can/should grab for context 257 | # Format and truncate (if necessary). 258 | context = int(settings.get('popup_char_context', 120)) - (col2 - col) - 1 259 | start = region[1] - context 260 | col_start = col2 - context 261 | overage = 0 262 | line = view.line(region[0]) 263 | if start < line.begin(): 264 | overage = line.begin() - start 265 | start = line.begin() 266 | col_start = 0 267 | end = region[1] + overage 268 | col_end = col2 + overage 269 | if end > line.end(): 270 | end = line.end() 271 | col_end = view.rowcol(line.end())[1] 272 | 273 | # Get line of code with bracket and emphasize the bracket 274 | content = view.substr(sublime.Region(start, end)) 275 | if re.match(r'#([\da-fA-F]{3}){1,2}', bracket_em): 276 | highlight_open = '' % bracket_em 277 | else: 278 | highlight_open = '' % bracket_em 279 | content = ( 280 | self.escape_code(content[:col - col_start], tab_size) + 281 | highlight_open + 282 | self.escape_code(content[col - col_start: col2 - col_start], tab_size) + 283 | '' + 284 | self.escape_code(content[col2 - col_start:], tab_size) 285 | ) 286 | 287 | # Get additional lines of context (if required) and format text 288 | if row != view.rowcol(point)[0]: 289 | line_context = int(int(settings.get('popup_line_context', 2)) / 2) 290 | content = self.get_multiline_context(view, content, row, col_start, col_end, tab_size, line_context) 291 | else: 292 | content = content.strip() 293 | 294 | # Put together the markup to show 295 | markup = '
%s
\n' % content 296 | markup += '\n' + '[(jump to bracket - line: %d)](%d)' % (row + 1, region[0]) 297 | 298 | return markup 299 | 300 | def show_popup(self, view, point, region, icon): 301 | """Show the popup.""" 302 | 303 | if HOVER_SUPPORT: 304 | if not self.is_bracket_visible(view, region): 305 | markup = self.get_markup(view, point, region, icon) 306 | 307 | self.popup_view = view 308 | mdpopups.show_popup( 309 | view, 310 | markup, 311 | wrapper_class=WRAPPER_CLASS, 312 | css=CSS, 313 | flags=sublime.HIDE_ON_MOUSE_MOVE_AWAY, 314 | max_width=800, 315 | location=point, 316 | on_navigate=self.on_navigate, 317 | template_vars={'mdpopups_version': mdpopups.version()}, 318 | template_env_options=template_options 319 | ) 320 | -------------------------------------------------------------------------------- /bh_wrapping.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from os.path import basename, splitext 10 | import re 11 | 12 | 13 | BH_TABSTOPS = re.compile(r"(\$\{BH_(SEL|TAB)(?:\:([^\}]+))?\})") 14 | TAB_REGION = "bh_plugin_wrapping_tabstop" 15 | SEL_REGION = "bh_plugin_wrapping_select" 16 | OUT_REGION = "bh_plugin_wrapping_outlier" 17 | 18 | VALID_INSERT_STYLES = ( 19 | ("inline", "Inline Insert"), 20 | ("block", "Block Insert"), 21 | ("indent_block", "Indented Block Insert") 22 | ) 23 | 24 | 25 | def exclude_entry(enabled, filter_type, language_list, language): 26 | """Exclude bracket wrapping entry by filter.""" 27 | 28 | exclude = True 29 | if enabled: 30 | # Black list languages 31 | if filter_type == 'blocklist': 32 | exclude = False 33 | if language is not None: 34 | for item in language_list: 35 | if language == item.lower(): 36 | exclude = True 37 | break 38 | # White list languages 39 | elif filter_type == 'allowlist': 40 | if language is not None: 41 | for item in language_list: 42 | if language == item.lower(): 43 | exclude = False 44 | break 45 | return exclude 46 | 47 | 48 | class WrapInstance(object): 49 | """Track wrap instance globally.""" 50 | 51 | obj = None 52 | value = None 53 | 54 | @classmethod 55 | def clear(cls): 56 | """Clear attributes.""" 57 | 58 | cls.obj = None 59 | cls.value = None 60 | 61 | 62 | class TextInsertion(object): 63 | """Wrapper class for inserting text.""" 64 | 65 | def __init__(self, view, edit): 66 | """Store view and edit objects.""" 67 | 68 | self.view = view 69 | self.edit = edit 70 | 71 | def insert(self, pt, text): 72 | """Perform insertion.""" 73 | 74 | return self.view.insert(self.edit, pt, text) 75 | 76 | 77 | class ExecuteWrapInstanceCommand(sublime_plugin.TextCommand): 78 | """Execute the wrap instance.""" 79 | 80 | def run(self, edit): 81 | """Call wrap instance.""" 82 | obj = WrapInstance.obj 83 | value = WrapInstance.value 84 | # Wrap selections with brackets 85 | style = obj._style[value] 86 | obj.insert_regions = [] 87 | 88 | for sel in obj.view.sel(): 89 | # Determine indentation style 90 | if style == "indent_block": 91 | obj.block(edit, sel, True) 92 | elif style == "block": 93 | obj.block(edit, sel) 94 | else: 95 | obj.inline(edit, sel) 96 | 97 | obj.select(edit) 98 | 99 | 100 | class WrapBrackets(object): 101 | """Wrap the current selection(s) with the defined wrapping options.""" 102 | 103 | def __init__(self, view, setting_file, attribute): 104 | """Initialization.""" 105 | 106 | self.view = view 107 | self._menu = [] 108 | self._brackets = [] 109 | self._insert = [] 110 | self._style = [] 111 | self.read_wrap_entries(setting_file, attribute) 112 | 113 | def inline(self, edit, sel): 114 | """Inline wrap.""" 115 | 116 | ti = TextInsertion(self.view, edit) 117 | 118 | offset1 = ti.insert(sel.begin(), self.brackets[0]) 119 | self.insert_regions.append(sublime.Region(sel.begin(), sel.begin() + offset1)) 120 | offset2 = ti.insert(sel.end() + offset1, self.brackets[1]) 121 | self.insert_regions.append(sublime.Region(sel.end() + offset1, sel.end() + offset1 + offset2)) 122 | 123 | def block(self, edit, sel, indent=False): 124 | """Wrap brackets around selection and block off the content.""" 125 | 126 | # Calculate number of lines between brackets 127 | self.calculate_lines(sel) 128 | # Calculate the current indentation of first bracket 129 | self.calculate_indentation(sel) 130 | 131 | ti = TextInsertion(self.view, edit) 132 | 133 | line_offset = 0 134 | first_end = 0 135 | second_end = 0 136 | second_start = sel.end() 137 | 138 | for b in reversed(self.brackets[1].split('\n')): 139 | second_end += ti.insert(sel.end(), "\n" + self.indent_to_col + b) 140 | num_open_lines = self.brackets[0].count('\n') 141 | for b in reversed(self.brackets[0].split('\n')): 142 | if line_offset == num_open_lines: 143 | line = b + "\n" 144 | else: 145 | line = self.indent_to_col + b + "\n" 146 | first_end += ti.insert(sel.begin(), line) 147 | line_offset += 1 148 | self.insert_regions.append(sublime.Region(sel.begin(), sel.begin() + first_end)) 149 | 150 | if indent: 151 | second_start += self.indent_content(ti, line_offset) 152 | else: 153 | pt = self.view.text_point(self.first_line + line_offset, 0) 154 | second_start += ti.insert(pt, self.indent_to_col) 155 | 156 | self.insert_regions.append(sublime.Region(first_end + second_start, first_end + second_start + second_end)) 157 | 158 | def indent_content(self, ti, line_offset): 159 | """Indent the block content.""" 160 | 161 | first = True 162 | offset = 0 163 | for l in range(line_offset, self.total_lines + line_offset): 164 | pt = self.view.text_point(self.first_line + l, 0) 165 | if first: 166 | offset += ti.insert(pt, self.indent_to_col + "\t") 167 | first = False 168 | else: 169 | offset += ti.insert(pt, "\t") 170 | return offset 171 | 172 | def calculate_lines(self, sel): 173 | """Calculate lines between brackets.""" 174 | 175 | self.first_line, self.col_position = self.view.rowcol(sel.begin()) 176 | last_line = self.view.rowcol(sel.end())[0] 177 | self.total_lines = last_line - self.first_line + 1 178 | 179 | def calculate_indentation(self, sel): 180 | """Calculate how much lines should be indented.""" 181 | 182 | tab_size = self.view.settings().get("tab_size", 4) 183 | tab_count = self.view.substr(sublime.Region(sel.begin() - self.col_position, sel.begin())).count('\t') 184 | spaces = self.col_position - tab_count 185 | self.indent_to_col = ( 186 | "\t" * tab_count + "\t" * int(spaces / tab_size) + 187 | " " * int(spaces % tab_size if spaces >= tab_size else spaces) 188 | ) 189 | 190 | def select(self, edit): 191 | """Select defined regions after wrapping.""" 192 | 193 | self.view.sel().clear() 194 | self.view.sel().add_all(self.insert_regions) 195 | 196 | final_sel = [] 197 | initial_sel = [] 198 | for s in self.view.sel(): 199 | string = self.view.substr(s) 200 | matches = [m for m in BH_TABSTOPS.finditer(string)] 201 | multi_offset = 0 202 | if matches: 203 | for m in matches: 204 | r = sublime.Region(s.begin() + multi_offset + m.start(1), s.begin() + multi_offset + m.end(1)) 205 | if m.group(3): 206 | replace = m.group(3) 207 | self.view.erase(edit, r) 208 | added = self.view.insert(edit, r.begin(), replace) 209 | final_sel.append( 210 | sublime.Region( 211 | s.begin() + multi_offset + m.start(1), 212 | s.begin() + multi_offset + m.start(1) + added 213 | ) 214 | ) 215 | multi_offset += added - r.size() 216 | else: 217 | self.view.erase(edit, r) 218 | final_sel.append(sublime.Region(s.begin() + multi_offset + m.start(1))) 219 | multi_offset -= r.size() 220 | if m.group(2) == "SEL": 221 | initial_sel.append(final_sel[-1]) 222 | 223 | if len(initial_sel) != len(final_sel): 224 | self.view.add_regions(TAB_REGION, final_sel, "", "", sublime.HIDDEN) 225 | 226 | # Re-position cursor 227 | self.view.sel().clear() 228 | if len(initial_sel): 229 | self.view.sel().add_all(initial_sel) 230 | elif len(final_sel): 231 | self.view.sel().add(final_sel[0]) 232 | 233 | def read_wrap_entries(self, setting_file, attribute): 234 | """Read wrap entries from the settings file.""" 235 | 236 | settings = sublime.load_settings(setting_file) 237 | syntax = self.view.settings().get('syntax') 238 | language = splitext(basename(syntax))[0].lower() if syntax is not None else "plain text" 239 | wrapping = settings.get(attribute, []) 240 | for i in wrapping: 241 | if not exclude_entry(i["enabled"], i["language_filter"], i["language_list"], language): 242 | for j in i.get("entries", []): 243 | try: 244 | menu_entry = j["name"] 245 | bracket_entry = j["brackets"] 246 | insert_style = j.get("insert_style", ["inline"]) 247 | self._menu.append(menu_entry) 248 | self._brackets.append(bracket_entry) 249 | self._insert.append(insert_style) 250 | except Exception: 251 | pass 252 | 253 | def wrap_brackets(self, value): 254 | """Wrap selection(s) with defined brackets.""" 255 | 256 | if value < 0: 257 | return 258 | 259 | # Use new edit object since the main run has already exited 260 | # and the old edit is more than likely closed now 261 | WrapInstance.obj = self 262 | WrapInstance.value = value 263 | 264 | self.view.run_command("execute_wrap_instance") 265 | 266 | WrapInstance.clear() 267 | 268 | def wrap_style(self, value): 269 | """Choose insert style for wrapping.""" 270 | 271 | if value < 0: 272 | return 273 | 274 | style = [] 275 | 276 | self.brackets = self._brackets[value] 277 | for s in VALID_INSERT_STYLES: 278 | if s[0] in self._insert[value]: 279 | self._style.append(s[0]) 280 | style.append(s[1]) 281 | 282 | if len(style) > 1: 283 | sublime.set_timeout( 284 | lambda: 285 | self.view.window().show_quick_panel( 286 | style, 287 | self.wrap_brackets 288 | ), 289 | 0 290 | ) 291 | else: 292 | self.wrap_brackets(0) 293 | 294 | 295 | class WrapBracketsCommand(sublime_plugin.TextCommand, WrapBrackets): 296 | """Bracket wrapping command.""" 297 | 298 | def run(self, edit): 299 | """Display the wrapping menu.""" 300 | 301 | self._menu = [] 302 | self._brackets = [] 303 | self._insert = [] 304 | self._style = [] 305 | self.read_wrap_entries("bh_wrapping.sublime-settings", "wrapping") 306 | 307 | if len(self._menu): 308 | self.view.window().show_quick_panel( 309 | self._menu, 310 | self.wrap_style 311 | ) 312 | 313 | def is_enabled(self, **kwargs): 314 | """Check if command is enabled.""" 315 | 316 | settings = self.view.settings() 317 | return bool( 318 | not settings.get('bracket_highlighter.ignore', False) and 319 | ( 320 | not settings.get('is_widget') or 321 | sublime.load_settings("bh_core.sublime-settings").get('search_in_widgets', False) 322 | ) 323 | ) 324 | 325 | 326 | class BhNextWrapSelCommand(sublime_plugin.TextCommand): 327 | """Navigate wrapping tab stop regions.""" 328 | 329 | def run(self, edit): 330 | """Look for the next wrapping tab stop region.""" 331 | 332 | regions = self.view.get_regions(SEL_REGION) + self.view.get_regions(OUT_REGION) 333 | if len(regions): 334 | self.view.sel().clear() 335 | self.view.sel().add_all(regions) 336 | 337 | # Clean up unneeded sections 338 | self.view.erase_regions(SEL_REGION) 339 | self.view.erase_regions(OUT_REGION) 340 | 341 | 342 | class BhWrapListener(sublime_plugin.EventListener): 343 | """Listen for wrapping tab stop tabbing.""" 344 | 345 | def on_query_context(self, view, key, operator, operand, match_all): 346 | """Mark the next regions to navigate to.""" 347 | 348 | accept_query = False 349 | if key == "bh_wrapping": 350 | select = [] 351 | outlier = [] 352 | regions = view.get_regions(TAB_REGION) 353 | tabstop = [] 354 | sels = view.sel() 355 | 356 | if len(regions) == 0: 357 | return False 358 | 359 | for s in sels: 360 | count = 0 361 | found = False 362 | for r in regions[:]: 363 | if found: 364 | select.append(r) 365 | tabstop.append(r) 366 | del regions[count] 367 | break 368 | if r.begin() <= s.begin() <= r.end(): 369 | del regions[count] 370 | found = True 371 | continue 372 | count += 1 373 | if not found: 374 | outlier.append(s) 375 | tabstop += regions 376 | 377 | if len(tabstop) == len(select): 378 | if len(tabstop): 379 | tabstop = [] 380 | accept_query = True 381 | elif len(tabstop) != 0: 382 | accept_query = True 383 | 384 | # Mark regions to make the "next" command aware of what to do 385 | view.add_regions(SEL_REGION, select, "", "", sublime.HIDDEN) 386 | view.add_regions(OUT_REGION, outlier, "", "", sublime.HIDDEN) 387 | view.add_regions(TAB_REGION, tabstop, "", "", sublime.HIDDEN) 388 | 389 | return accept_query 390 | -------------------------------------------------------------------------------- /bh_modules/tags.py: -------------------------------------------------------------------------------- 1 | """ 2 | BracketHighlighter. 3 | 4 | Copyright (c) 2013 - 2016 Isaac Muse 5 | License: MIT 6 | """ 7 | from backrefs import bre 8 | from collections import namedtuple 9 | import sublime 10 | from os.path import basename, splitext 11 | 12 | TAG_OPEN = 0 13 | TAG_CLOSE = 1 14 | 15 | last_mode = None 16 | 17 | 18 | def process_tag_pattern(pattern, variables=None): 19 | """Process the tag pattern.""" 20 | 21 | if variables is None: 22 | variables = {} 23 | 24 | if isinstance(pattern, str): 25 | pattern = bre.compile_search(pattern % variables, bre.I | bre.M) 26 | return pattern 27 | 28 | 29 | class TagEntry(namedtuple('TagEntry', ['begin', 'end', 'name', 'optional', 'single'])): 30 | """Tag entry tuple.""" 31 | 32 | def move(self, begin, end): 33 | """Create a new tuple from this tuple.""" 34 | 35 | return self._replace(begin=begin, end=end) 36 | 37 | 38 | def compare_languge(language, lang_list): 39 | """Check if language is found.""" 40 | 41 | found = False 42 | for l in lang_list: 43 | if language == l.lower(): 44 | found = True 45 | break 46 | return found 47 | 48 | 49 | def get_tag_mode(view, tag_mode_config): 50 | """Get the tag mode.""" 51 | 52 | default_mode = None 53 | syntax = view.settings().get('syntax') 54 | language = splitext(basename(syntax))[0].lower() if syntax is not None else "plain text" 55 | if isinstance(tag_mode_config, list): 56 | for item in tag_mode_config: 57 | if isinstance(item, dict) and compare_languge(language, item.get('syntax', [])): 58 | first_line = item.get('first_line', '') 59 | if first_line: 60 | size = view.size() - 1 61 | if size > 256: 62 | size = 256 63 | if ( 64 | isinstance(first_line, str) and 65 | bre.compile_search(first_line, bre.I).match(view.substr(sublime.Region(0, size))) 66 | ): 67 | return item.get('mode', default_mode) 68 | else: 69 | return item.get('mode', default_mode) 70 | return default_mode 71 | 72 | 73 | def highlighting(view, name, style, left, right): 74 | """Highlight only the tag name.""" 75 | tag_settings = sublime.load_settings("bh_tag.sublime-settings") 76 | match_style = tag_settings.get("tag_style", {}).get(last_mode, None) 77 | if match_style is not None and style == match_style: 78 | tag_name = tag_settings.get('tag_name', {}).get(last_mode, r'[\w\:\.\-]+') 79 | if left is not None: 80 | region = view.find(tag_name, left.begin) 81 | left = left.move(region.begin(), region.end()) 82 | if right is not None: 83 | region = view.find(tag_name, right.begin) 84 | right = right.move(region.begin(), region.end()) 85 | return left, right 86 | 87 | 88 | def post_match(view, name, style, first, second, center, bfr, threshold): 89 | """ 90 | Given two brackets, determine if they contain a tag. 91 | 92 | Decide whether it is an opening or closing, and then 93 | find its respective closing or opening. 94 | """ 95 | 96 | # We need to know the mode during the highlight event, so track the last mode. 97 | global last_mode 98 | left, right = first, second 99 | threshold = [0, len(bfr)] if threshold is None else threshold 100 | bh_settings = sublime.load_settings("bh_core.sublime-settings") 101 | tag_settings = sublime.load_settings("bh_tag.sublime-settings") 102 | tag_mode = get_tag_mode(view, tag_settings.get("tag_mode", [])) 103 | tag_style = tag_settings.get("tag_style", {}).get(tag_mode, '?') 104 | last_mode = tag_mode 105 | outside_adj = bh_settings.get("bracket_outside_adjacent", False) 106 | 107 | bracket_style = style 108 | 109 | if first is not None and tag_mode is not None: 110 | matcher = TagMatch(view, bfr, threshold, first, second, center, outside_adj, tag_mode) 111 | left, right = matcher.match() 112 | if not matcher.no_tag: 113 | bracket_style = tag_style 114 | 115 | return left, right, bracket_style 116 | 117 | 118 | class TagSearch(object): 119 | """Searches for tags.""" 120 | 121 | def __init__( 122 | self, view, bfr, window, center, pattern, 123 | match_type, mode, optional_tags, self_closing_tags, void_tags 124 | ): 125 | """Prepare tag search object.""" 126 | 127 | self.start = int(window[0]) 128 | self.end = int(window[1]) 129 | self.optional_tags = optional_tags 130 | self.void_tags = void_tags 131 | self.self_closing_tags = self_closing_tags 132 | self.center = center 133 | self.pattern = pattern 134 | self.match_type = match_type 135 | self.mode = mode 136 | self.bfr = bfr 137 | self.prev_match = None 138 | self.return_prev = False 139 | self.done = False 140 | self.view = view 141 | settings = sublime.load_settings("bh_tag.sublime-settings") 142 | try: 143 | self.scope_exclude = settings.get("tag_scope_exclude", {}).get(mode, ['string', 'comment']) 144 | except Exception: 145 | self.scope_exclude = ['string', 'comment'] 146 | 147 | def scope_check(self, pt): 148 | """Check if scope is good.""" 149 | 150 | illegal_scope = False 151 | for exclude in self.scope_exclude: 152 | illegal_scope |= bool(self.view.score_selector(pt, exclude)) 153 | return illegal_scope 154 | 155 | def reset_end_state(self): 156 | """Reset and end the current state.""" 157 | 158 | self.done = False 159 | self.prev_match = None 160 | self.return_prev = False 161 | 162 | def remember(self): 163 | """Instruct object to return the last tag.""" 164 | 165 | self.return_prev = True 166 | self.done = False 167 | 168 | def get_tags(self): 169 | """Find all the tags.""" 170 | 171 | if self.done: 172 | return 173 | if self.return_prev: 174 | self.return_prev = False 175 | yield self.prev_match 176 | for m in self.pattern.finditer(self.bfr, self.start, self.end): 177 | name = m.group(1).lower() 178 | if not self.match_type: 179 | self_closing_slash = bool(m.group(2) != "") 180 | if not self_closing_slash and self.optional_tags is not None: 181 | optional = self.optional_tags.match(name) is not None 182 | else: 183 | optional = False 184 | 185 | if self_closing_slash and self.self_closing_tags is not None: 186 | self_closing = self.self_closing_tags.match(name) is not None 187 | else: 188 | self_closing = False 189 | 190 | if not optional and not self_closing and self.void_tags is not None: 191 | void = self.void_tags.match(name) is not None 192 | else: 193 | void = False 194 | 195 | else: 196 | if self.void_tags is not None and self.void_tags.match(name) is not None: 197 | continue 198 | void = False 199 | optional = False 200 | self_closing = False 201 | start = m.start(0) 202 | end = m.end(0) 203 | if not self.scope_check(start): 204 | self.prev_match = TagEntry(start, end, name, optional, void or self_closing) 205 | self.start = end 206 | yield self.prev_match 207 | self.done = True 208 | 209 | 210 | class TagMatch(object): 211 | """Find a tag match.""" 212 | 213 | def __init__(self, view, bfr, threshold, first, second, center, outside_adj, mode): 214 | """Prepare tag match object.""" 215 | 216 | tag_settings = sublime.load_settings('bh_tag.sublime-settings') 217 | self.view = view 218 | self.bfr = bfr 219 | self.mode = mode 220 | self.tag_open = process_tag_pattern( 221 | tag_settings.get("start_tag")[mode], 222 | { 223 | "attributes": tag_settings.get('attributes', {}).get(mode, ''), 224 | "tag_name": tag_settings.get('tag_name', {}).get(mode, '') 225 | } 226 | ) 227 | 228 | self.tag_close = process_tag_pattern( 229 | tag_settings.get("end_tag")[mode] 230 | ) 231 | 232 | try: 233 | self.optional_tags = bre.compile_search(tag_settings.get('optional_tag_patterns')[self.mode], bre.I) 234 | except Exception: 235 | self.optional_tags = None 236 | 237 | try: 238 | self.void_tags = bre.compile_search(tag_settings.get('void_tag_patterns')[self.mode], bre.I) 239 | except Exception: 240 | self.void_tags = None 241 | 242 | try: 243 | self.self_closing_tags = bre.compile_search(tag_settings.get('self_closing_tag_patterns')[self.mode], bre.I) 244 | except Exception: 245 | self.self_closing_tags = None 246 | 247 | tag, tag_type, tag_end = self.get_first_tag(first[0]) 248 | self.left, self.right = None, None 249 | self.window = None 250 | self.no_tag = False 251 | if outside_adj: 252 | if first[0] == center: 253 | center += 1 254 | elif center == tag_end: 255 | center -= 1 256 | if tag and first[0] < center < tag_end: 257 | if tag.single: 258 | self.left = tag 259 | self.right = tag 260 | else: 261 | if tag_type == "open": 262 | self.left = tag 263 | self.window = (tag_end, len(bfr) if threshold is None else threshold[1]) 264 | else: 265 | self.right = tag 266 | self.window = (0 if threshold is None else threshold[0], first[0]) 267 | else: 268 | self.left = first 269 | self.right = second 270 | self.no_tag = True 271 | 272 | def get_first_tag(self, offset): 273 | """ 274 | Check if tag region is an opening tag or closing tag. 275 | 276 | Return the results 277 | """ 278 | 279 | tag = None 280 | tag_type = None 281 | optional = False 282 | void = False 283 | m = self.tag_open.match(self.bfr[offset:]) 284 | end = None 285 | if m: 286 | name = m.group(1).lower() 287 | self_closing_slash = bool(m.group(2) != "") 288 | optional = ( 289 | self.optional_tags is not None and 290 | not self_closing_slash and 291 | self.optional_tags.match(name) is not None 292 | ) 293 | self_closing = ( 294 | self.self_closing_tags is not None and 295 | self_closing_slash and 296 | self.self_closing_tags.match(name) is not None 297 | ) 298 | void = ( 299 | not optional and 300 | not self_closing and 301 | self.void_tags is not None and 302 | self.void_tags.match(name) is not None 303 | ) 304 | start = m.start(0) + offset 305 | end = m.end(0) + offset 306 | tag = TagEntry(start, end, name, optional, void or self_closing) 307 | tag_type = "open" 308 | self.center = end 309 | else: 310 | m = self.tag_close.match(self.bfr[offset:]) 311 | if m: 312 | name = m.group(1).lower() 313 | void = ( 314 | self.void_tags is not None and 315 | self.void_tags.match(name) is not None 316 | ) 317 | if not void: 318 | start = m.start(0) + offset 319 | end = m.end(0) + offset 320 | tag = TagEntry(start, end, name, optional, void) 321 | tag_type = "close" 322 | self.center = offset 323 | return tag, tag_type, end 324 | 325 | def compare_tags(self, left, right): 326 | """Check if tags share the same name.""" 327 | 328 | return left.name == right.name 329 | 330 | def resolve_optional(self, stack, c): 331 | """Handle self closing tags.""" 332 | 333 | found_tag = None 334 | b = stack[-1] 335 | if self.compare_tags(b, c): 336 | found_tag = b 337 | stack.pop() 338 | else: 339 | while b is not None and b.optional: 340 | stack.pop() 341 | if len(stack): 342 | b = stack[-1] 343 | if self.compare_tags(b, c): 344 | found_tag = b 345 | stack.pop() 346 | break 347 | else: 348 | b = None 349 | return found_tag 350 | 351 | def match(self): 352 | """ 353 | Find the corresponding open or close. 354 | 355 | Match only if either the close or open is already found. 356 | """ 357 | 358 | stack = [] 359 | 360 | # No tags to search for 361 | if self.no_tag or (self.left and self.right): 362 | return self.left, self.right 363 | 364 | # Initialize tag matching objects 365 | osearch = TagSearch( 366 | self.view, self.bfr, self.window, 367 | self.center, self.tag_open, 368 | 0, self.mode, 369 | self.optional_tags, 370 | self.self_closing_tags, 371 | self.void_tags 372 | ) 373 | csearch = TagSearch( 374 | self.view, self.bfr, self.window, 375 | self.center, self.tag_close, 376 | 1, self.mode, 377 | self.optional_tags, 378 | self.self_closing_tags, 379 | self.void_tags 380 | ) 381 | 382 | # Searching for opening or closing tag to match 383 | match_type = TAG_OPEN if self.right else TAG_CLOSE 384 | 385 | # Match the tags 386 | for c in csearch.get_tags(): 387 | if len(stack) and osearch.done: 388 | if self.resolve_optional(stack, c): 389 | continue 390 | for o in osearch.get_tags(): 391 | if o.end <= c.begin: 392 | if not o.single: 393 | stack.append(o) 394 | continue 395 | else: 396 | osearch.remember() 397 | break 398 | 399 | if len(stack): 400 | if self.resolve_optional(stack, c): 401 | continue 402 | elif match_type == TAG_OPEN and not osearch.done: 403 | continue 404 | if match_type == TAG_CLOSE: 405 | if self.left is None or self.compare_tags(self.left, c): 406 | self.right = c 407 | elif self.left.optional: 408 | self.right = self.left 409 | break 410 | 411 | if match_type == TAG_OPEN: 412 | # Find the rest of the the unmatched left side open brackets 413 | # approaching the cursor if all closing brackets were matched 414 | # Select the most recent open bracket on the stack. 415 | for o in osearch.get_tags(): 416 | if not o.single: 417 | stack.append(o) 418 | if len(stack): 419 | self.left = self.resolve_optional(stack, self.right) 420 | elif self.right is None and self.left is not None and self.left.optional: 421 | # Account for the opening tag that was found being a self closing 422 | self.right = self.left 423 | 424 | return self.left, self.right 425 | --------------------------------------------------------------------------------