├── .python-version ├── lib ├── __init__.py └── file_strip │ ├── __init__.py │ ├── json.py │ └── comments.py ├── run_tests.sh ├── tests ├── __init__.py ├── test_json.py └── validate_json_format.py ├── messages.json ├── .github ├── FUNDING.yml ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ ├── deploy.yml │ └── build.yml ├── labels.yml └── ISSUE_TEMPLATE.md ├── .gitattributes ├── .gitignore ├── dependencies.json ├── messages ├── install.md └── recent.md ├── Default.sublime-commands ├── tox.ini ├── scope_hunter_notify.py ├── readme.md ├── scope_hunter.sublime-settings ├── Main.sublime-menu ├── popup.j2 ├── .pyspelling.yml ├── mkdocs.yml ├── support.py ├── CHANGES.md └── scope_hunter.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.13 2 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | """ScopeHunter lib.""" 2 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | py.test . 2 | flake8 . 3 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Unit Tests.""" 2 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "2.19.0": "messages/recent.md" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: facelessuser 2 | custom: 3 | - "https://www.paypal.me/facelessuser" 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Specify filepatterns you want to assign special attributes. 2 | docs/ export-ignore 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.patch 3 | site/* 4 | *.DS_Store 5 | build/* 6 | *.cache 7 | .pytest_cache 8 | .tox/** 9 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | ">=3124": [ 4 | "mdpopups" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/file_strip/__init__.py: -------------------------------------------------------------------------------- 1 | """File Strip.""" 2 | from . import comments 3 | from . import json 4 | 5 | __all__ = ('comments', 'json') 6 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please follow this link to see Contributing & Support documentation: http://facelessuser.github.io/ScopeHunter/contributing/. 2 | -------------------------------------------------------------------------------- /messages/install.md: -------------------------------------------------------------------------------- 1 | # ScopeHunter 2 | 3 | Welcome to ScopeHunter! For a quick start guide, please go to 4 | `Preferences->Package Settings->ScopeHunter->Quick Start Guide`. 5 | -------------------------------------------------------------------------------- /messages/recent.md: -------------------------------------------------------------------------------- 1 | # ScopeHunter 2.18.0 2 | 3 | New release! 4 | 5 | See `Preferences->Package Settings->ScopeHunter->Changelog` for more info on previous releases. 6 | 7 | ## 2.19.0 8 | 9 | - **NEW**: Changes for Python 3.13 on ST 4201+. 10 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | // Get Selection Scope 3 | { 4 | "caption": "Scope Hunter: Show Scope Under Cursor(s)", 5 | "command": "get_selection_scope" 6 | }, 7 | // Toggle Instant Scoper 8 | { 9 | "caption": "Scope Hunter: Toggle Instant Scoper", 10 | "command": "toggle_selection_scope" 11 | } 12 | ] 13 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist=True 3 | envlist = 4 | py39,py310,py311,py312,py313,py314,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,E741,N818 31 | max-line-length=120 32 | exclude=site/*.py,.tox/*,lib/png.py 33 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thanks you for contributing to this project! Make sure you've read: http://facelessuser.github.io/ScopeHunter/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 | 8 | jobs: 9 | 10 | documents: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: 3.11 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip setuptools build 25 | python -m pip install -r docs/src/requirements.txt 26 | - name: Deploy documents 27 | run: | 28 | git config user.name facelessuser 29 | git config user.email "${{ secrets.GH_EMAIL }}" 30 | git remote add gh-token "https://${{ secrets.GH_TOKEN }}@github.com/facelessuser/ScopeHunter.git" 31 | git fetch gh-token && git fetch gh-token gh-pages:gh-pages 32 | python -m mkdocs gh-deploy -v --clean --remote-name gh-token 33 | git push gh-token gh-pages 34 | -------------------------------------------------------------------------------- /scope_hunter_notify.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scope Hunter. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012 - 2016 Isaac Muse 6 | """ 7 | import sublime 8 | try: 9 | from SubNotify.sub_notify import SubNotifyIsReadyCommand as Notify 10 | except Exception: 11 | class Notify(object): 12 | """Fallback Notify class if SubNotify is not found.""" 13 | 14 | @classmethod 15 | def is_ready(cls): 16 | """Return False to disable SubNotify.""" 17 | 18 | return False 19 | 20 | 21 | def notify(msg): 22 | """Notify message.""" 23 | 24 | settings = sublime.load_settings("scope_hunter.sublime-settings") 25 | if settings.get("use_sub_notify", False) and Notify.is_ready(): 26 | sublime.run_command("sub_notify", {"title": "ScopeHunter", "msg": msg}) 27 | else: 28 | sublime.status_message(msg) 29 | 30 | 31 | def error(msg): 32 | """Error message.""" 33 | 34 | settings = sublime.load_settings("scope_hunter.sublime-settings") 35 | if settings.get("use_sub_notify", False) and Notify.is_ready(): 36 | sublime.run_command("sub_notify", {"title": "ScopeHunter", "msg": msg, "level": "error"}) 37 | else: 38 | sublime.error_message("ScopeHunter:\n%s" % msg) 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 8 | rules: 9 | - labels: ['C: infrastructure'] 10 | patterns: ['*|-@(*.md|*.py|*.sublime-@(keymap|menu|settings|commands|color-scheme))', '.github/**'] 11 | 12 | - labels: ['C: source'] 13 | patterns: ['**/@(*.py|*.sublime-@(keymap|menu|settings|commands|color-scheme))|-tests'] 14 | 15 | - labels: ['C: docs'] 16 | patterns: ['**/*.md|docs/**'] 17 | 18 | - labels: ['C: tests'] 19 | patterns: ['tests/**'] 20 | 21 | - labels: ['C: popups'] 22 | patterns: ['*.j2'] 23 | 24 | - labels: ['C: scheme-handling'] 25 | patterns: ['lib/color_scheme*.py|lib/rgba.py|lib/x11colors.py'] 26 | 27 | - labels: ['C: notify'] 28 | patterns: ['scope_hunter_notify.py'] 29 | 30 | - labels: ['C: settings'] 31 | patterns: ['*.sublime-@(keymap|menu|settings|commands|color-scheme)'] 32 | 33 | # Label management 34 | 35 | labels: 36 | - name: 'C: popups' 37 | color: subcategory 38 | description: Related popups. 39 | 40 | - name: 'C: scheme-handling' 41 | renamed: scheme-handling 42 | color: subcategory 43 | description: Related to scheme handling. 44 | 45 | - name: 'C: notify' 46 | renamed: notify 47 | color: subcategory 48 | description: Related to notifications. 49 | 50 | - name: 'C: settings' 51 | renamed: settings 52 | color: subcategory 53 | description: Related to Sublime settings. 54 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | [![Donate via PayPal][donate-image]][donate-link] 2 | [![Package Control Downloads][pc-image]][pc-link] 3 | ![License][license-image] 4 | # ScopeHunter 5 | 6 | This is a simple plugin that can get the scope under the cursor(s) in Sublime Text. This plugin is useful for plugin 7 | development. 8 | 9 | ![Screenshot 1](docs/src/markdown/images/screenshot1.png) 10 | 11 | ![Screenshot 2](docs/src/markdown/images/screenshot2.png) 12 | 13 | ## Features 14 | All features are configurable via the settings file 15 | 16 | - Tooltip output showing scope, context backtrace, scope extent, color values, links to current syntax and relevant 17 | color schemes. 18 | - Customizable to show only the information you care about. 19 | - Auto copy scope to clipboard on execution. 20 | - Quick copy any or all information to the clipboard. 21 | - Toggle instant scoping to keep showing scope as you move around a file. 22 | - Supports [SubNotify](https://github.com/facelessuser/SubNotify) messages. 23 | 24 | # Documentation 25 | 26 | https://facelessuser.github.io/ScopeHunter/ 27 | 28 | # License 29 | 30 | Scope Hunter is released under the MIT license. 31 | 32 | [pc-image]: https://img.shields.io/packagecontrol/dt/ScopeHunter.svg?labelColor=333333&logo=sublime%20text 33 | [pc-link]: https://packagecontrol.io/packages/ScopeHunter 34 | [license-image]: https://img.shields.io/badge/license-MIT-blue.svg?labelColor=333333 35 | [donate-image]: https://img.shields.io/badge/Donate-PayPal-3fabd1?logo=paypal 36 | [donate-link]: https://www.paypal.me/facelessuser 37 | -------------------------------------------------------------------------------- /.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 build 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 build 50 | - name: Lint 51 | run: | 52 | python -m tox 53 | 54 | documents: 55 | 56 | env: 57 | TOXENV: documents 58 | 59 | runs-on: ubuntu-latest 60 | 61 | steps: 62 | - uses: actions/checkout@v4 63 | - name: Set up Python 64 | uses: actions/setup-python@v4 65 | with: 66 | python-version: 3.11 67 | - name: Install dependencies 68 | run: | 69 | python -m pip install --upgrade pip setuptools tox build 70 | - name: Install Aspell 71 | run: | 72 | sudo apt-get install aspell aspell-en 73 | - name: Build documents 74 | run: | 75 | python -m tox 76 | -------------------------------------------------------------------------------- /.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/ScopeHunter/) 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 link to color scheme used (with link if a 3rd party color scheme) if applicable. 24 | 25 | ## Support Info 26 | 27 | ... 28 | 29 | > Run the following command from the menu: `Preferences->Package Settings->ScopeHunter->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 | -------------------------------------------------------------------------------- /scope_hunter.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | /////////////////////////// 3 | // Dev Options 4 | /////////////////////////// 5 | "debug": false, 6 | 7 | /////////////////////////// 8 | // Additional Scope Info 9 | /////////////////////////// 10 | 11 | // Show the scope backtrace (ST >= 4087) 12 | "context_backtrace": true, 13 | 14 | // Show scope extent in point format 15 | "extent_points": false, 16 | 17 | // Show scope extent in line/char format 18 | "extent_line_char": false, 19 | 20 | // Show color and style at the given point 21 | "styling": false, 22 | 23 | // Show current syntax and color scheme paths 24 | // (click to open if using tooltips) 25 | "file_paths": false, 26 | 27 | /////////////////////////// 28 | // Highlight Configuration 29 | /////////////////////////// 30 | 31 | // Highlight scope extent in view 32 | "highlight_extent": true, 33 | 34 | // Scope to use for the color 35 | "highlight_scope": "invalid", 36 | 37 | // Highlight style (underline|solid|outline|thin_underline|squiggly|stippled) 38 | "highlight_style": "outline", 39 | 40 | /////////////////////////// 41 | // Additional Options 42 | /////////////////////////// 43 | 44 | // Automatically copy scopes to clipboard 45 | "clipboard": false, 46 | 47 | // Allow multi-select scope hunting 48 | "multiselect": true, 49 | 50 | // Max region size to highlight 51 | "highlight_max_size": 100, 52 | 53 | // Use SubNotify plugin messages if installed 54 | "use_sub_notify": true, 55 | 56 | /////////////////////////// 57 | // Graphics 58 | /////////////////////////// 59 | 60 | // By default, image border is calculated based on theme background, but if for 61 | // some reason, it isn't sufficient in your popup, set it to any color using 62 | // valid CSS for RGB, HSL, or HWB colors. 63 | "image_border_color": null 64 | } 65 | -------------------------------------------------------------------------------- /lib/file_strip/json.py: -------------------------------------------------------------------------------- 1 | # noqa: A005 2 | """ 3 | File Strip. 4 | 5 | Licensed under MIT 6 | Copyright (c) 2012 - 2016 Isaac Muse 7 | """ 8 | import re 9 | from .comments import Comments 10 | 11 | JSON_PATTERN = re.compile( 12 | r'''(?x) 13 | ( 14 | (?P 15 | , # trailing comma 16 | (?P[\s\r\n]*) # white space 17 | (?P\]) # bracket 18 | ) 19 | | (?P 20 | , # trailing comma 21 | (?P[\s\r\n]*) # white space 22 | (?P\}) # bracket 23 | ) 24 | ) 25 | | (?P 26 | "(?:\\.|[^"\\])*" # double quoted string 27 | | '(?:\\.|[^'\\])*' # single quoted string 28 | | .[^,"']* # everything else 29 | ) 30 | ''', 31 | re.DOTALL 32 | ) 33 | 34 | 35 | def strip_dangling_commas(text, preserve_lines=False): 36 | """Strip dangling commas.""" 37 | 38 | regex = JSON_PATTERN 39 | 40 | def remove_comma(g, preserve_lines): 41 | """Remove comma.""" 42 | 43 | if preserve_lines: 44 | # ,] -> ] else ,} -> } 45 | if g["square_comma"] is not None: 46 | return g["square_ws"] + g["square_bracket"] 47 | else: 48 | return g["curly_ws"] + g["curly_bracket"] 49 | else: 50 | # ,] -> ] else ,} -> } 51 | return g["square_bracket"] if g["square_comma"] else g["curly_bracket"] 52 | 53 | def evaluate(m, preserve_lines): 54 | """Search for dangling comma.""" 55 | 56 | g = m.groupdict() 57 | return remove_comma(g, preserve_lines) if g["code"] is None else g["code"] 58 | 59 | return ''.join(map(lambda m: evaluate(m, preserve_lines), regex.finditer(text))) 60 | 61 | 62 | def strip_comments(text, preserve_lines=False): 63 | """Strip JavaScript like comments.""" 64 | 65 | return Comments('json', preserve_lines).strip(text) 66 | 67 | 68 | def sanitize_json(text, preserve_lines=False): 69 | """Sanitize the JSON file by removing comments and dangling commas.""" 70 | 71 | return strip_dangling_commas(Comments('json', preserve_lines).strip(text), preserve_lines) 72 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "preferences", 4 | "children": 5 | [ 6 | { 7 | "caption": "Package Settings", 8 | "id": "package-settings", 9 | "children": 10 | [ 11 | { 12 | "caption": "ScopeHunter", 13 | "children": 14 | [ 15 | { 16 | "command": "edit_settings", "args": 17 | { 18 | "base_file": "${packages}/ScopeHunter/scope_hunter.sublime-settings", 19 | "default": "{\n\t$0\n}\n" 20 | }, 21 | "caption": "Settings" 22 | }, 23 | { "caption": "-" }, 24 | { 25 | "caption": "Changelog", 26 | "command": "scope_hunter_changes" 27 | }, 28 | { 29 | "caption": "Documentation", 30 | "command": "scope_hunter_open_site", 31 | "args": { 32 | "url": "https://facelessuser.github.io/ScopeHunter/" 33 | } 34 | }, 35 | // { 36 | // "caption": "Quick Start Guide", 37 | // "command": "scope_hunter_doc", 38 | // "args": { 39 | // "page": "${packages}/ScopeHunter/quickstart.md" 40 | // } 41 | // }, 42 | { "caption": "-" }, 43 | { 44 | "caption": "Support Info", 45 | "command": "scope_hunter_support_info" 46 | }, 47 | { 48 | "caption": "Issues", 49 | "command": "scope_hunter_open_site", 50 | "args": { 51 | "url": "https://github.com/facelessuser/ScopeHunter/issues" 52 | } 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ] 59 | } 60 | ] 61 | -------------------------------------------------------------------------------- /popup.j2: -------------------------------------------------------------------------------- 1 | ### Scope [copy](copy-scope:{{plugin.scope_index}}){: .small .button} {: .header} 2 | {{plugin.scope}} 3 | 4 | {% if plugin.context_backtrace %} 5 | ### Scope Context Backtrace [copy](copy-context-backtrace:{{plugin.context_backtrace_index}}){: .small .button} {: .header} 6 | {% for ctx in plugin.context_backtrace_stack %} 7 | **{{loop.index}}:**{: .keyword} {{ctx}} 8 | 9 | {% endfor %} 10 | {% endif %} 11 | 12 | {% if plugin.pt_extent or plugin.rowcol_extent %} 13 | ### Scope Extent {: .header} 14 | {% if plugin.pt_extent %} 15 | **pts:**{: .keyword} ({{plugin.extent_start}}, {{plugin.extent_end}}) [copy](copy-points:{{plugin.extent_pt_index}}){: .small .button} 16 | 17 | {% endif %} 18 | {% if plugin.pt_extent or plugin.rowcol_extent %} 19 | **line:char:**{: .keyword} ({{plugin.l_start}}:{{plugin.c_start}}, {{plugin.l_end}}:{{plugin.c_end}}) [copy](copy-line-char:{{plugin.line_char_index}}){: .small .button} 20 | {% endif %} 21 | {% endif %} 22 | 23 | {% if plugin.appearance %} 24 | ### Appearance {: .header} 25 | **fg:**{: .keyword} {{plugin.fg_preview}} {{plugin.fg_color}} [copy](copy-fg:{{plugin.fg_index}}){: .small .button} 26 | 27 | {% if plugin.fg_sim %} 28 | **fg (simulated alpha):**{: .keyword} {{plugin.fg_sim_preview}} {{plugin.fg_sim_color}} [copy](copy-fg-sim:{{plugin.fg_sim_index}}){: .small .button} 29 | 30 | {% endif %} 31 | {% if plugin.fg_hash %} 32 | **hashed fg:**{: .keyword} {{plugin.fg_hash_preview}} {{plugin.fg_hash_color}} [copy](copy-fg-hash:{{plugin.fg_hash_index}}){: .small .button} 33 | 34 | {% endif %} 35 | {% if plugin.fg_hash_sim %} 36 | **hashed fg (simulated alpha):**{: .keyword} {{plugin.fg_hash_sim_preview}} {{plugin.fg_hash_sim_color}} [copy](copy-fg-hash-sim:{{plugin.fg_hash_sim_index}}){: .small .button} 37 | 38 | {% endif %} 39 | **bg:**{: .keyword} {{plugin.bg_preview}} {{plugin.bg_color}} [copy](copy-bg:{{plugin.bg_index}}){: .small .button} 40 | 41 | {% if plugin.bg_sim %} 42 | **bg (simulated alpha):**{: .keyword} {{plugin.bg_sim_preview}} {{plugin.bg_sim_color}} [copy](copy-bg-sim:{{plugin.bg_sim_index}}){: .small .button} 43 | 44 | {% endif %} 45 | **style:**{: .keyword} {{plugin.style_open}}{{plugin.style}}{{plugin.style_close}} [copy](copy-style:{{plugin.style_index}}){: .small .button} 46 | 47 | {% endif %} 48 | 49 | {% if plugin.files %} 50 | ### Files {: .header} 51 | **syntax:**{: .keyword} [{{plugin.syntax}}](syntax) [copy](copy-syntax:{{plugin.syntax_index}}){: .small .button} 52 | 53 | {% if plugin.scheme %} 54 | **scheme:**{: .keyword} [{{plugin.scheme}}](scheme) [copy](copy-scheme:{{plugin.scheme_index}}){: .small .button} 55 | {% endif %} 56 | 57 | {% for item in plugin.overrides %} 58 | **scheme {{loop.index}}:**{: .keyword} [{{item}}](override:{{plugin.overrides_index}}:{{loop.index}}) [copy](copy-overrides:{{plugin.overrides_index}}:{{loop.index}}){: .small .button} 59 | 60 | {% endfor %} 61 | {% endif %} 62 | -------------------------------------------------------------------------------- /.pyspelling.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | - name: settings 3 | sources: 4 | - '**/*.sublime-settings' 5 | hunspell: 6 | d: en_US 7 | aspell: 8 | lang: en 9 | dictionary: 10 | wordlists: 11 | - docs/src/dictionary/en-custom.txt 12 | output: build/dictionary/settings.dic 13 | pipeline: 14 | - pyspelling.filters.cpp: 15 | prefix: 'st' 16 | generic_mode: true 17 | group_comments: true 18 | line_comments: false 19 | - pyspelling.filters.context: 20 | context_visible_first: true 21 | escapes: '\\[\\`]' 22 | delimiters: 23 | # Ignore multiline content between fences (fences can have 3 or more back ticks) 24 | # ``` 25 | # content 26 | # ``` 27 | - open: '(?s)^(?P *`{3,})$' 28 | close: '^(?P=open)$' 29 | # Ignore text between inline back ticks 30 | - open: '(?P`+)' 31 | close: '(?P=open)' 32 | - pyspelling.filters.url: 33 | 34 | - name: mkdocs 35 | sources: 36 | - site/**/*.html 37 | hunspell: 38 | d: en_US 39 | aspell: 40 | lang: en 41 | dictionary: 42 | wordlists: 43 | - docs/src/dictionary/en-custom.txt 44 | output: build/dictionary/mkdocs.dic 45 | pipeline: 46 | - pyspelling.filters.html: 47 | comments: false 48 | attributes: 49 | - title 50 | - alt 51 | ignores: 52 | - 'code, pre' 53 | - '.magiclink-compare, .magiclink-commit, .magiclink-repository, .md-social__link' 54 | - 'span.keys' 55 | - '.MathJax_Preview, .md-nav__link, .md-footer-custom-text, .md-source__repository, .headerlink, .md-icon' 56 | - pyspelling.filters.url: 57 | 58 | - name: markdown 59 | sources: 60 | - readme.md 61 | hunspell: 62 | d: en_US 63 | aspell: 64 | lang: en 65 | dictionary: 66 | wordlists: 67 | - docs/src/dictionary/en-custom.txt 68 | output: build/dictionary/markdown.dic 69 | pipeline: 70 | - pyspelling.filters.markdown: 71 | - pyspelling.filters.html: 72 | comments: false 73 | attributes: 74 | - title 75 | - alt 76 | ignores: 77 | - ':matches(code, pre)' 78 | - pyspelling.filters.url: 79 | 80 | - name: python 81 | hidden: true 82 | sources: 83 | - './**/*.py' 84 | hunspell: 85 | d: en_US 86 | aspell: 87 | lang: en 88 | dictionary: 89 | wordlists: 90 | - docs/src/dictionary/en-custom.txt 91 | output: build/dictionary/python.dic 92 | pipeline: 93 | - pyspelling.filters.python: 94 | group_comments: true 95 | - pyspelling.flow_control.wildcard: 96 | allow: 97 | - py-comment 98 | - pyspelling.filters.context: 99 | context_visible_first: true 100 | delimiters: 101 | # Ignore lint (noqa) and coverage (pragma) as well as shebang (#!) 102 | - open: '^(?: *(?:noqa\b|pragma: no cover)|!)' 103 | close: '$' 104 | # Ignore Python encoding string -*- encoding stuff -*- 105 | - open: '^ *-\*-' 106 | close: '-\*-$' 107 | - pyspelling.filters.context: 108 | context_visible_first: true 109 | escapes: '\\[\\`]' 110 | delimiters: 111 | # Ignore multiline content between fences (fences can have 3 or more back ticks) 112 | # ``` 113 | # content 114 | # ``` 115 | - open: '(?s)^(?P *`{3,})$' 116 | close: '^(?P=open)$' 117 | # Ignore text between inline back ticks 118 | - open: '(?P`+)' 119 | close: '(?P=open)' 120 | - pyspelling.filters.url: 121 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: ScopeHunter Documentation 2 | site_url: https://facelessuser.github.io/ScopeHunter 3 | repo_url: https://github.com/facelessuser/ScopeHunter 4 | edit_uri: tree/master/docs/src/markdown 5 | site_description: Syntax Scope Viewer in Sublime Text. 6 | copyright: | 7 | Copyright © 2012 - 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 | - navigation.instant 26 | 27 | nav: 28 | - Home: 29 | - ScopeHunter: index.md 30 | - Installation: installation.md 31 | - User Guide: usage.md 32 | - About: 33 | - Contributing & Support: about/contributing.md 34 | - Changelog: https://github.com/facelessuser/ScopeHunter/blob/master/CHANGES.md 35 | - License: about/license.md 36 | 37 | markdown_extensions: 38 | - markdown.extensions.toc: 39 | slugify: !!python/name:pymdownx.slugs.uslugify 40 | permalink: "" 41 | - markdown.extensions.smarty: 42 | smart_quotes: false 43 | - pymdownx.betterem: 44 | - markdown.extensions.attr_list: 45 | - markdown.extensions.tables: 46 | - markdown.extensions.abbr: 47 | - markdown.extensions.footnotes: 48 | - markdown.extensions.md_in_html: 49 | - pymdownx.superfences: 50 | - pymdownx.highlight: 51 | extend_pygments_lang: 52 | - name: pycon3 53 | lang: pycon 54 | options: 55 | python3: true 56 | - pymdownx.inlinehilite: 57 | - pymdownx.magiclink: 58 | repo_url_shortener: true 59 | repo_url_shorthand: true 60 | social_url_shorthand: true 61 | user: facelessuser 62 | repo: ScopeHunter 63 | - pymdownx.tilde: 64 | - pymdownx.caret: 65 | - pymdownx.smartsymbols: 66 | - pymdownx.emoji: 67 | emoji_index: !!python/name:material.extensions.emoji.twemoji 68 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 69 | - pymdownx.escapeall: 70 | hardbreak: true 71 | nbsp: true 72 | - pymdownx.tasklist: 73 | custom_checkbox: true 74 | - pymdownx.arithmatex: 75 | - pymdownx.mark: 76 | - pymdownx.striphtml: 77 | - pymdownx.snippets: 78 | base_path: docs/src/markdown/_snippets 79 | - pymdownx.keys: 80 | separator: "\uff0b" 81 | - pymdownx.saneheaders: 82 | - pymdownx.blocks.admonition: 83 | types: 84 | - new 85 | - settings 86 | - note 87 | - abstract 88 | - info 89 | - tip 90 | - success 91 | - question 92 | - warning 93 | - failure 94 | - danger 95 | - bug 96 | - example 97 | - quote 98 | - pymdownx.blocks.details: 99 | types: 100 | - name: details-new 101 | class: new 102 | - name: details-settings 103 | class: settings 104 | - name: details-note 105 | class: note 106 | - name: details-abstract 107 | class: abstract 108 | - name: details-info 109 | class: info 110 | - name: details-tip 111 | class: tip 112 | - name: details-success 113 | class: success 114 | - name: details-question 115 | class: question 116 | - name: details-warning 117 | class: warning 118 | - name: details-failure 119 | class: failure 120 | - name: details-danger 121 | class: danger 122 | - name: details-bug 123 | class: bug 124 | - name: details-example 125 | class: example 126 | - name: details-quote 127 | class: quote 128 | - pymdownx.blocks.html: 129 | - pymdownx.blocks.definition: 130 | - pymdownx.blocks.tab: 131 | alternate_style: True 132 | 133 | extra: 134 | social: 135 | - icon: fontawesome/brands/github 136 | link: https://github.com/facelessuser 137 | 138 | plugins: 139 | - search 140 | - git-revision-date-localized 141 | - mkdocs_pymdownx_material_extras 142 | - minify: 143 | minify_html: true 144 | -------------------------------------------------------------------------------- /lib/file_strip/comments.py: -------------------------------------------------------------------------------- 1 | """ 2 | File Strip. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012 - 2016 Isaac Muse 6 | """ 7 | import re 8 | 9 | LINE_PRESERVE = re.compile(r"\r?\n", re.MULTILINE) 10 | CSS_PATTERN = re.compile( 11 | r'''(?x) 12 | (?P 13 | /\*[^*]*\*+(?:[^/*][^*]*\*+)*/ # multi-line comments 14 | ) 15 | | (?P 16 | "(?:\\.|[^"\\])*" # double quotes 17 | | '(?:\\.|[^'\\])*' # single quotes 18 | | .[^/"']* # everything else 19 | ) 20 | ''', 21 | re.DOTALL 22 | ) 23 | CPP_PATTERN = re.compile( 24 | r'''(?x) 25 | (?P 26 | /\*[^*]*\*+(?:[^/*][^*]*\*+)*/ # multi-line comments 27 | | \s*//(?:[^\r\n])* # single line comments 28 | ) 29 | | (?P 30 | "(?:\\.|[^"\\])*" # double quotes 31 | | '(?:\\.|[^'\\])*' # single quotes 32 | | .[^/"']* # everything else 33 | ) 34 | ''', 35 | re.DOTALL 36 | ) 37 | PY_PATTERN = re.compile( 38 | r'''(?x) 39 | (?P 40 | \s*\#(?:[^\r\n])* # single line comments 41 | ) 42 | | (?P 43 | "{3}(?:\\.|[^\\])*"{3} # triple double quotes 44 | | '{3}(?:\\.|[^\\])*'{3} # triple single quotes 45 | | "(?:\\.|[^"\\])*" # double quotes 46 | | '(?:\\.|[^'])*' # single quotes 47 | | .[^\#"']* # everything else 48 | ) 49 | ''', 50 | re.DOTALL 51 | ) 52 | 53 | 54 | def _strip_regex(pattern, text, preserve_lines): 55 | """Generic function that strips out comments pased on the given pattern.""" 56 | 57 | def remove_comments(group, preserve_lines=False): 58 | """Remove comments.""" 59 | 60 | return ''.join([x[0] for x in LINE_PRESERVE.findall(group)]) if preserve_lines else '' 61 | 62 | def evaluate(m, preserve_lines): 63 | """Search for comments.""" 64 | 65 | g = m.groupdict() 66 | return g["code"] if g["code"] is not None else remove_comments(g["comments"], preserve_lines) 67 | 68 | return ''.join(map(lambda m: evaluate(m, preserve_lines), pattern.finditer(text))) 69 | 70 | 71 | @staticmethod 72 | def _cpp(text, preserve_lines=False): 73 | """C/C++ style comment stripper.""" 74 | 75 | return _strip_regex( 76 | CPP_PATTERN, 77 | text, 78 | preserve_lines 79 | ) 80 | 81 | 82 | @staticmethod 83 | def _python(text, preserve_lines=False): 84 | """Python style comment stripper.""" 85 | 86 | return _strip_regex( 87 | PY_PATTERN, 88 | text, 89 | preserve_lines 90 | ) 91 | 92 | 93 | @staticmethod 94 | def _css(text, preserve_lines=False): 95 | """CSS style comment stripper.""" 96 | 97 | return _strip_regex( 98 | CSS_PATTERN, 99 | text, 100 | preserve_lines 101 | ) 102 | 103 | 104 | class CommentException(Exception): 105 | """Comment exception.""" 106 | 107 | def __init__(self, value): 108 | """Setup exception.""" 109 | 110 | self.value = value 111 | 112 | def __str__(self): 113 | """Return exception value repr on string convert.""" 114 | 115 | return repr(self.value) 116 | 117 | 118 | class Comments(object): 119 | """Comment strip class.""" 120 | 121 | styles = [] 122 | 123 | def __init__(self, style=None, preserve_lines=False): 124 | """Initialize.""" 125 | 126 | self.preserve_lines = preserve_lines 127 | self.call = self.__get_style(style) 128 | 129 | @classmethod 130 | def add_style(cls, style, fn): 131 | """Add comment style.""" 132 | 133 | if style not in cls.__dict__: 134 | setattr(cls, style, fn) 135 | cls.styles.append(style) 136 | 137 | def __get_style(self, style): 138 | """Get the comment style.""" 139 | 140 | if style in self.styles: 141 | return getattr(self, style) 142 | else: 143 | raise CommentException(style) 144 | 145 | def strip(self, text): 146 | """Strip comments.""" 147 | 148 | return self.call(text, self.preserve_lines) 149 | 150 | 151 | Comments.add_style("c", _cpp) 152 | Comments.add_style("json", _cpp) 153 | Comments.add_style("cpp", _cpp) 154 | Comments.add_style("python", _python) 155 | Comments.add_style("css", _css) 156 | -------------------------------------------------------------------------------- /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.19.0" 9 | __pc_name__ = 'ScopeHunter' 10 | 11 | 12 | CSS = ''' 13 | div.scope-hunter { padding: 10px; margin: 0; } 14 | .scope-hunter h1, .scope-hunter h2, .scope-hunter h3, 15 | .scope-hunter h4, .scope-hunter h5, .scope-hunter h6 { 16 | {{'string'|css}} 17 | } 18 | .scope-hunter blockquote { {{'comment'|css}} } 19 | .scope-hunter a { text-decoration: none; } 20 | ''' 21 | 22 | frontmatter = { 23 | "markdown_extensions": [ 24 | "markdown.extensions.admonition", 25 | "markdown.extensions.attr_list", 26 | "markdown.extensions.def_list", 27 | "markdown.extensions.nl2br", 28 | # Smart quotes always have corner cases that annoy me, so don't bother with them. 29 | {"markdown.extensions.smarty": {"smart_quotes": False}}, 30 | "pymdownx.betterem", 31 | { 32 | "pymdownx.magiclink": { 33 | "repo_url_shortener": True, 34 | "repo_url_shorthand": True, 35 | "user": "facelessuser", 36 | "repo": "HexViewer" 37 | } 38 | }, 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 ScopeHunterSupportInfoCommand(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 | msg = textwrap.dedent( 97 | """\ 98 | - ST ver.: {version} 99 | - Platform: {platform} 100 | - Arch: {arch} 101 | - Plugin ver.: {plugin_version} 102 | - Install via PC: {pc_install} 103 | - mdpopups ver.: {mdpopups_version} 104 | """.format(**info) 105 | ) 106 | 107 | sublime.message_dialog(msg + '\nInfo has been copied to the clipboard.') 108 | sublime.set_clipboard(msg) 109 | 110 | 111 | class ScopeHunterOpenSiteCommand(sublime_plugin.ApplicationCommand): 112 | """Open site links.""" 113 | 114 | def run(self, url): 115 | """Open the url.""" 116 | 117 | webbrowser.open_new_tab(url) 118 | 119 | 120 | class ScopeHunterDocCommand(sublime_plugin.WindowCommand): 121 | """Open doc page.""" 122 | 123 | re_pkgs = re.compile(r'^Packages') 124 | 125 | def on_navigate(self, href): 126 | """Handle links.""" 127 | 128 | if href.startswith('sub://Packages'): 129 | sublime.run_command('open_file', {"file": self.re_pkgs.sub('${packages}', href[6:])}) 130 | else: 131 | webbrowser.open_new_tab(href) 132 | 133 | def run(self, page): 134 | """Open page.""" 135 | 136 | try: 137 | import mdpopups 138 | has_phantom_support = (mdpopups.version() >= (1, 10, 0)) and (int(sublime.version()) >= 3124) 139 | fmatter = mdpopups.format_frontmatter(frontmatter) 140 | except Exception: 141 | fmatter = '' 142 | has_phantom_support = False 143 | 144 | if not has_phantom_support: 145 | sublime.run_command('open_file', {"file": page}) 146 | else: 147 | text = sublime.load_resource(page.replace('${packages}', 'Packages')) 148 | view = self.window.new_file() 149 | view.set_name('ScopeHunter - Quick Start') 150 | view.settings().set('gutter', False) 151 | view.settings().set('word_wrap', False) 152 | if has_phantom_support: 153 | mdpopups.add_phantom( 154 | view, 155 | 'quickstart', 156 | sublime.Region(0), 157 | fmatter + text, 158 | sublime.LAYOUT_INLINE, 159 | css=CSS, 160 | wrapper_class="scope-hunter", 161 | on_navigate=self.on_navigate 162 | ) 163 | else: 164 | view.run_command('insert', {"characters": text}) 165 | view.set_read_only(True) 166 | view.set_scratch(True) 167 | 168 | 169 | class ScopeHunterChangesCommand(sublime_plugin.WindowCommand): 170 | """Changelog command.""" 171 | 172 | def run(self): 173 | """Show the changelog in a new view.""" 174 | try: 175 | import mdpopups 176 | has_phantom_support = (mdpopups.version() >= (1, 10, 0)) and (int(sublime.version()) >= 3124) 177 | fmatter = mdpopups.format_frontmatter(frontmatter) 178 | except Exception: 179 | fmatter = '' 180 | has_phantom_support = False 181 | 182 | text = sublime.load_resource('Packages/ScopeHunter/CHANGES.md') 183 | view = self.window.new_file() 184 | view.set_name('ScopeHunter - Changelog') 185 | view.settings().set('gutter', False) 186 | view.settings().set('word_wrap', False) 187 | if has_phantom_support: 188 | mdpopups.add_phantom( 189 | view, 190 | 'changelog', 191 | sublime.Region(0), 192 | fmatter + text, 193 | sublime.LAYOUT_INLINE, 194 | wrapper_class="scope-hunter", 195 | css=CSS, 196 | on_navigate=self.on_navigate 197 | ) 198 | else: 199 | view.run_command('insert', {"characters": text}) 200 | view.set_read_only(True) 201 | view.set_scratch(True) 202 | 203 | def on_navigate(self, href): 204 | """Open links.""" 205 | webbrowser.open_new_tab(href) 206 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # ScopeHunter 2 | 3 | ## 2.19.0 4 | 5 | - **NEW**: Changes for Python 3.13 on ST 4201+. 6 | 7 | ## 2.18.3 8 | 9 | - **FIX**: Add typing dependency. 10 | 11 | ## 2.18.2 12 | 13 | - **FIX**: Fix color API access failures. 14 | 15 | ## 2.18.1 16 | 17 | - **FIX**: Scope backtrace fixes. 18 | 19 | ## 2.18.0 20 | 21 | - **NEW**: Refactor scope backtrace to be in the same way with Sublime's default implementation. 22 | 23 | ## 2.17.0 24 | 25 | - **NEW**: Update context backtrace to support changes to API in 4127. 26 | 27 | ## 2.16.3 28 | 29 | - **FIX**: Remove unnecessary dependencies. 30 | 31 | ## 2.16.2 32 | 33 | - **FIX**: Ensure ready for Package Control 4.0 (compatibility issues with latest `mdpopups`). 34 | 35 | ## 2.16.1 36 | 37 | - **FIX**: Fix issue with latest refactor where a variable was not always defined. 38 | 39 | ## 2.16 40 | 41 | - **NEW**: Due to Sublime schemes ever evolving, there were a few things (like "forward fill" scopes) that we didn't 42 | have support for. These implementation details are hard to reverse engineer, so to make support easier moving 43 | forward, we now use Sublime's `View.style()` to get the style at a given point instead of manually parsing the 44 | scheme ourselves. This means we no longer provide the original defined colors from the scheme file, but instead only 45 | the end result after overlaying transparent colors etc. Because of this, `show_simulated_alpha_colors` option has 46 | been removed. 47 | - **NEW**: Because we are no longer parsing the scheme files ourselves anymore, we can no longer provide contributing 48 | scopes to individual style components. The related `selectors` option has been removed. 49 | - **FIX**: Fix issue with Sublime 4095 `auto` light/dark color scheme resolution. 50 | - **FIX**: Reduce dependencies by relying on the `coloraide` in `mdpopups` which we already include. 51 | - **FIX**: Remove old `tooltip_theme` option that hasn't been used in quite some time. 52 | 53 | ## 2.15.2 54 | 55 | - **FIX**: Better styling for popups. 56 | - **FIX**: `tmTheme` support compressed hex; therefore, ScopeHunter must account for these colors. 57 | - **FIX**: Fix false positive on hashed foreground colors. 58 | 59 | ## 2.15.1 60 | 61 | - **FIX**: Fix issue with support commands. 62 | 63 | ## 2.15.0 64 | 65 | - **NEW**: Format dialog a little more compact. 66 | - **NEW**: Require new `coloraide` dependency. With this dependency, schemes that use `min-contrast` should work now. 67 | - **NEW**: ScopeHunter now only shows information in tooltip. Showing info in separate panel and console has been 68 | dropped as tooltip functionality is available on all latest Sublime versions. 69 | - **NEW**: Backtrace info available in Sublime Text build 4087. 70 | - **NEW**: Add `image_border_color` option. 71 | - **FIX**: Fix bug with copying color scheme name. 72 | - **FIX**: Fix some issues related to schemes (Celeste theme) using invalid colors, such as `"none"` to reset 73 | background colors etc. 74 | 75 | ## 2.14.0 76 | 77 | - **NEW**: Add support for `glow` and `underline` styles. 78 | - **FIX**: Fix font style reporting in popups. 79 | 80 | ## 2.13.1 81 | 82 | - **FIX**: ST4 now handles `HSL` properly, remove workaround for build 4069. 83 | - **FIX**: `+`/`-` have to be followed by spaces in `saturation`, `lightness`, and `alpha` or they should be treated 84 | as part of the number following them. `*` does not need a space. 85 | - **FIX**: Add support for `deg` unit type for the hue channel with `HSL` and `HWB`. 86 | - **FIX**: Sublime will ignore the unit types `rad`, `grad`, and `turn` for `HSL` and `HWB`, but add support for them 87 | in case Sublime ever does. 88 | 89 | ## 2.13.0 90 | 91 | - **NEW**: Add support for blending colors in the `HSL` and `HWB` color spaces in color schemes. Sublime has a bug 92 | where it blends in these color spaces in a surprising way. We do not fully match it, but we will not currently fail 93 | anymore. 94 | - **NEW**: Support `+`, `-`, and `*` in `alpha()`/`a()`. 95 | - **NEW**: Support `lightness()` and `saturation()`. 96 | - **NEW**: Support `foreground_adjust` in color schemes. 97 | 98 | ## 2.12.0 99 | 100 | - **NEW**: Add support for color scheme `alpha()`/`a()` blend and `hwb()` colors. 101 | 102 | ## 2.11.1 103 | 104 | - **FIX**: Allow `-` in variables names. Write color translations to main scheme object and ensure filtering is done 105 | after color translations. 106 | 107 | ## 2.11.0 108 | 109 | - **NEW**: Add support for `.hidden-color-scheme`. 110 | 111 | ## 2.10. 112 | 113 | - **FIX**: Create fallback file read for resource race condition. 114 | 115 | ## 2.10. 116 | 117 | - **FIX**: Parse legacy `foregroundSelection` properly. 118 | 119 | ## 2.10. 120 | 121 | - **NEW**: Add support `.sublime-color-scheme` hashed syntax highlighting. 122 | - **FIX**: Copy of color entries. 123 | - **FIX**: `.sublime-color-scheme` merge logic. 124 | 125 | ## 2.9.3 126 | 127 | - **FIX**: Parse color schemes properly when extension is unexpected. 128 | 129 | ## 2.9.2 130 | 131 | - **FIX**: Support for irregular `.sublime-color-scheme` values. 132 | 133 | ## 2.9.1 134 | 135 | - **FIX**: Scheme parsing related fixes. 136 | 137 | ## 2.9.0 138 | 139 | - **NEW**: Handle overrides for new color scheme styles and bring back scope info for style attributes. 140 | - **NEW**: Hide names if no names available. 141 | - **NEW**: Small popup format tweaks. 142 | - **NEW**: Add option to manually refresh color scheme in cache. 143 | - **NEW**: Show overrides file names in panel and/or popup. 144 | - **FIX**: Font style read error when no font style. 145 | 146 | ## 2.8.0 147 | 148 | - **NEW**: Add support for `.sublime-color-scheme` (some features may not be available as scheme handling has 149 | changed). 150 | - **NEW**: Remove "Generate CSS" command as this feature is no longer relevant as schemes have drastically changed. 151 | - **NEW**: Update dependencies. 152 | - **FIX**: On 3150+, ScopeHunter will always give the latest colors (no cached scheme). 153 | - **FIX**: Ensure both bold and italic is shown for style when both are set for a selector. 154 | - **FIX**: Small fixes in color matcher lib for builds <3150. 155 | 156 | ## 2.7.0 157 | 158 | - **NEW**: Popups now require ST 3124+. 159 | - **FIX**: Fix scope matching issues. 160 | 161 | ## 2.6.0 162 | 163 | - **NEW**: Add support for X11 color names in color schemes. 164 | - **NEW**: Add new support commands. 165 | - **FIX**: Protect against race condition (#34) 166 | 167 | ## 2.5.6 168 | 169 | - **FIX**: Failure when evaluating bold text (!33) 170 | 171 | ## 2.5.5 172 | 173 | - **FIX**: Some CSS tweaks. 174 | 175 | ## 2.5.4 176 | 177 | - **FIX**: Guard against loading mdpopups on old Sublime versions. 178 | 179 | ## 2.5.3 180 | 181 | - **FIX**: Fix changelog typo :). 182 | 183 | ## 2.5.2 184 | 185 | - **FIX**: Incorrect logic regarding bold. 186 | 187 | ## 2.5.1 188 | 189 | - **FIX**: Fix copy all link. 190 | 191 | ## 2.5.0 192 | 193 | - **NEW**: Changelog command available in `Package Settings->ScopeHunter`. Will render a full changelog in an HTML 194 | phantom in a new view. 195 | - **NEW**: Support info command available in `Package Settings->ScopeHunter`. 196 | - **NEW**: Will attempt to tell Package Control to update the most recent desired mdpopups. Really need to test that 197 | this actually does works :). 198 | - **NEW**: Requires `mdpopups` version 1.9.0. Run Package Control `Satisfy Dependencies` command if not already 199 | present. May require restart after update. 200 | - **FIX**: Formatting fixes. 201 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scope_hunter.py: -------------------------------------------------------------------------------- 1 | """ 2 | Scope Hunter. 3 | 4 | Licensed under MIT 5 | Copyright (c) 2012 - 2016 Isaac Muse 6 | """ 7 | import sublime 8 | import sublime_plugin 9 | from time import time, sleep 10 | import threading 11 | from ScopeHunter.scope_hunter_notify import notify 12 | from textwrap import dedent 13 | import mdpopups 14 | from collections import namedtuple 15 | from mdpopups.coloraide import Color 16 | import os 17 | 18 | AUTO = int(sublime.version()) >= 4095 19 | 20 | HEX = {"hex": True} 21 | HEX_NA = {"hex": True, "alpha": False} 22 | SRGB_SPACES = ('srgb', 'hsl', 'hwb') 23 | 24 | SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4087 = int(sublime.version()) >= 4087 25 | SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4127 = int(sublime.version()) >= 4127 26 | SCOPE_CONTEXT_BACKTRACE_SUPPORT = SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4127 or SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4087 27 | 28 | if 'sh_thread' not in globals(): 29 | sh_thread = None 30 | 31 | sh_settings = {} 32 | 33 | ADD_CSS = dedent( 34 | ''' 35 | html.light { 36 | --sh-button-color: color(var(--mdpopups-bg) blend(black 85%)); 37 | } 38 | html.dark { 39 | --sh-button-color: color(var(--mdpopups-bg) blend(white 85%)); 40 | } 41 | div.scope-hunter { margin: 0; padding: 0.5rem; } 42 | .scope-hunter .small { font-size: 0.8rem; } 43 | .scope-hunter .header { {{'.string'|css('color')}} } 44 | ins { text-decoration: underline; } 45 | span.glow { background-color: color(var(--foreground) a(0.2)); } 46 | div.color-helper { margin: 0; padding: 0rem; } 47 | .scope-hunter a.button { 48 | display: inline-block; 49 | padding: 0.25rem; 50 | color: var(--foreground); 51 | background-color: var(--sh-button-color); 52 | border-radius: 0.25rem; 53 | text-decoration: none; 54 | font-style: none; 55 | font-weight: normal; 56 | } 57 | .scope-hunter hr { 58 | border-color: var(--sh-button-color); 59 | } 60 | ''' 61 | ) 62 | 63 | COPY_ALL = ''' 64 | --- 65 | 66 | [Copy All](copy-all){: .small .button} 67 | ''' 68 | 69 | # Text Entry 70 | ENTRY = "{:30} {}" 71 | SCOPE_KEY = "Scope" 72 | CONTEXT_BACKTRACE_KEY = "Scope Context Backtrace" 73 | PTS_KEY = "Scope Extents (Pts)" 74 | PTS_VALUE = "({:d}, {:d})" 75 | CHAR_LINE_KEY = "Scope Extents (Line:Char)" 76 | CHAR_LINE_VALUE = "({:d}:{:d}, {:d}:{:d})" 77 | FG_KEY = "Fg" 78 | BG_KEY = "Bg" 79 | STYLE_KEY = "Style" 80 | FG_NAME_KEY = "Fg Name" 81 | FG_SCOPE_KEY = "Fg Scope" 82 | BG_NAME_KEY = "Bg Name" 83 | BG_SCOPE_KEY = "Bg Scope" 84 | BOLD_NAME_KEY = "Bold Name" 85 | BOLD_SCOPE_KEY = "Bold Scope" 86 | ITALIC_NAME_KEY = "Italic Name" 87 | ITALIC_SCOPE_KEY = "Italic Scope" 88 | UNDERLINE_NAME_KEY = "Underline Name" 89 | UNDERLINE_SCOPE_KEY = "Underline Scope" 90 | GLOW_NAME_KEY = "Glow Name" 91 | GLOW_SCOPE_KEY = "Glow Scope" 92 | SCHEME_KEY = "Scheme File" 93 | SYNTAX_KEY = "Syntax File" 94 | OVERRIDE_SCHEME_KEY = "Scheme" 95 | 96 | 97 | class SchemeColors( 98 | namedtuple( 99 | 'SchemeColors', 100 | [ 101 | 'fg', "bg", "style", "source", "line", "col" 102 | ] 103 | ) 104 | ): 105 | """Scheme colors.""" 106 | 107 | 108 | def log(msg): 109 | """Logging.""" 110 | print("ScopeHunter: {}".format(msg)) 111 | 112 | 113 | def debug(msg): 114 | """Debug.""" 115 | if sh_settings.get('debug', False): 116 | log(msg) 117 | 118 | 119 | def scheme_scope_format(scope): 120 | """Scheme scope format.""" 121 | 122 | return '\n\n{}'.format( 123 | '\n'.join( 124 | ['- {}'.format(x) for x in scope.split(',')] 125 | ) 126 | ) 127 | 128 | 129 | def extent_style(option): 130 | """Configure style of region based on option.""" 131 | 132 | style = sublime.HIDE_ON_MINIMAP 133 | if option == "outline": 134 | style |= sublime.DRAW_NO_FILL 135 | elif option == "none": 136 | style |= sublime.HIDDEN 137 | elif option == "underline": 138 | style |= sublime.DRAW_EMPTY_AS_OVERWRITE 139 | elif option == "thin_underline": 140 | style |= sublime.DRAW_NO_FILL 141 | style |= sublime.DRAW_NO_OUTLINE 142 | style |= sublime.DRAW_SOLID_UNDERLINE 143 | elif option == "squiggly": 144 | style |= sublime.DRAW_NO_FILL 145 | style |= sublime.DRAW_NO_OUTLINE 146 | style |= sublime.DRAW_SQUIGGLY_UNDERLINE 147 | elif option == "stippled": 148 | style |= sublime.DRAW_NO_FILL 149 | style |= sublime.DRAW_NO_OUTLINE 150 | style |= sublime.DRAW_STIPPLED_UNDERLINE 151 | return style 152 | 153 | 154 | def underline(regions): 155 | """Convert to empty regions.""" 156 | 157 | new_regions = [] 158 | for region in regions: 159 | start = region.begin() 160 | end = region.end() 161 | while start < end: 162 | new_regions.append(sublime.Region(start)) 163 | start += 1 164 | return new_regions 165 | 166 | 167 | def copy_data(bfr, label, index, copy_format=None): 168 | """Copy data to clipboard from buffer.""" 169 | 170 | line = bfr[index] 171 | if line.startswith(label + ':'): 172 | text = line.replace(label + ':', '', 1).strip() 173 | if copy_format is not None: 174 | text = copy_format(text) 175 | sublime.set_clipboard(text) 176 | notify("Copied: {}".format(label)) 177 | 178 | 179 | class ScopeHunterEditCommand(sublime_plugin.TextCommand): 180 | """Edit a view.""" 181 | 182 | bfr = None 183 | pt = None 184 | 185 | def run(self, edit): 186 | """Insert text into buffer.""" 187 | 188 | cls = ScopeHunterEditCommand 189 | self.view.insert(edit, cls.pt, cls.bfr) 190 | 191 | @classmethod 192 | def clear(cls): 193 | """Clear edit buffer.""" 194 | 195 | cls.bfr = None 196 | cls.pt = None 197 | 198 | 199 | class GetSelectionScope: 200 | """Get the scope and the selection(s).""" 201 | 202 | def setup(self, sh_settings): 203 | """Setup.""" 204 | 205 | self.show_out_of_gamut_preview = True 206 | self.setup_image_border(sh_settings) 207 | self.setup_sizes() 208 | 209 | def setup_image_border(self, sh_settings): 210 | """Setup_image_border.""" 211 | 212 | border_color = sh_settings.get('image_border_color') 213 | border_color = None 214 | if border_color is not None: 215 | try: 216 | border_color = Color(border_color, filters=SRGB_SPACES) 217 | border_color.fit("srgb", in_place=True) 218 | except Exception: 219 | border_color = None 220 | 221 | if border_color is None: 222 | # Calculate border color for images 223 | border_color = Color( 224 | self.view.style()['background'], 225 | filters=SRGB_SPACES 226 | ).convert("hsl") 227 | border_color['l'] = border_color['l'] + (30 if border_color.luminance() < 0.5 else -30) 228 | 229 | self.default_border = border_color.convert("srgb").to_string(**HEX) 230 | self.out_of_gamut = Color("transparent", filters=SRGB_SPACES).to_string(**HEX) 231 | self.out_of_gamut_border = Color( 232 | self.view.style().get('redish', "red"), 233 | filters=SRGB_SPACES 234 | ).to_string(**HEX) 235 | 236 | def setup_sizes(self): 237 | """Get sizes.""" 238 | 239 | # Calculate color box height 240 | self.line_height = self.view.line_height() 241 | top_pad = self.view.settings().get('line_padding_top', 0) 242 | bottom_pad = self.view.settings().get('line_padding_bottom', 0) 243 | if top_pad is None: 244 | # Sometimes we strangely get None 245 | top_pad = 0 246 | if bottom_pad is None: 247 | bottom_pad = 0 248 | box_height = self.line_height - int(top_pad + bottom_pad) - 6 249 | 250 | self.height = self.width = box_height * 2 251 | 252 | def check_size(self, height, scale=4): 253 | """Get checkered size.""" 254 | 255 | check_size = int((height - 2) / scale) 256 | if check_size < 2: 257 | check_size = 2 258 | return check_size 259 | 260 | def init_template_vars(self): 261 | """Initialize template variables.""" 262 | 263 | self.template_vars = {} 264 | 265 | def next_index(self): 266 | """Get next index into scope buffer.""" 267 | 268 | self.index += 1 269 | return self.index 270 | 271 | def get_color_box(self, color, key, index): 272 | """Display an HTML color box using the given color.""" 273 | 274 | border = self.default_border 275 | box_height = int(self.height) 276 | box_width = int(self.width) 277 | check_size = int(self.check_size(box_height)) 278 | if isinstance(color, list): 279 | box_width = box_width * (len(color) if len(color) >= 1 else 1) 280 | colors = [c.upper() for c in color] 281 | else: 282 | colors = [color.upper()] 283 | if check_size < 2: 284 | check_size = 2 285 | self.template_vars['{}_preview'.format(key)] = '{}'.format( 286 | mdpopups.color_box( 287 | colors, border, height=box_height, 288 | width=box_width, border_size=1, check_size=check_size 289 | ) 290 | ) 291 | self.template_vars['{}_color'.format(key)] = ', '.join(colors) 292 | self.template_vars['{}_index'.format(key)] = index 293 | 294 | def get_extents(self, pt): 295 | """Get the scope extent via the sublime API.""" 296 | 297 | pts = None 298 | file_end = self.view.size() 299 | scope_name = self.view.scope_name(pt) 300 | for r in self.view.find_by_selector(scope_name): 301 | if r.contains(pt): 302 | pts = r 303 | break 304 | elif pt == file_end and r.end() == pt: 305 | pts = r 306 | break 307 | 308 | if pts is None: 309 | pts = sublime.Region(pt) 310 | 311 | row1, col1 = self.view.rowcol(pts.begin()) 312 | row2, col2 = self.view.rowcol(pts.end()) 313 | 314 | # Scale back the extent by one for true points included 315 | if pts.size() < self.highlight_max_size: 316 | self.extents.append(sublime.Region(pts.begin(), pts.end())) 317 | 318 | if self.points_info or self.rowcol_info: 319 | if self.points_info: 320 | self.scope_bfr.append(ENTRY.format(PTS_KEY + ':', PTS_VALUE.format(pts.begin(), pts.end()))) 321 | if self.rowcol_info: 322 | self.scope_bfr.append( 323 | ENTRY.format(CHAR_LINE_KEY + ':', CHAR_LINE_VALUE.format(row1 + 1, col1 + 1, row2 + 1, col2 + 1)) 324 | ) 325 | 326 | if self.points_info: 327 | self.template_vars["pt_extent"] = True 328 | self.template_vars["extent_start"] = pts.begin() 329 | self.template_vars["extent_end"] = pts.end() 330 | self.template_vars["extent_pt_index"] = self.next_index() 331 | if self.rowcol_info: 332 | self.template_vars["rowcol_extent"] = True 333 | self.template_vars["l_start"] = row1 + 1 334 | self.template_vars["l_end"] = row2 + 1 335 | self.template_vars["c_start"] = col1 + 1 336 | self.template_vars["c_end"] = col2 + 1 337 | self.template_vars["line_char_index"] = self.next_index() 338 | 339 | def get_scope(self, pt): 340 | """Get the scope at the cursor.""" 341 | 342 | scope = self.view.scope_name(pt) 343 | spacing = "\n" + (" " * 31) 344 | 345 | if self.clipboard: 346 | self.clips.append(scope) 347 | 348 | self.scope_bfr.append(ENTRY.format(SCOPE_KEY + ':', self.view.scope_name(pt).strip().replace(" ", spacing))) 349 | 350 | self.template_vars['scope'] = '
'.join(self.view.scope_name(pt).strip().split(' ')) 351 | self.template_vars['scope_index'] = self.next_index() 352 | 353 | return scope 354 | 355 | def get_scope_context_backtrace(self, pt): 356 | """Get the context backtrace of the current scope.""" 357 | 358 | spacing = "\n" + (" " * 31) 359 | 360 | if SCOPE_CONTEXT_BACKTRACE_SUPPORT: 361 | stack = list(reversed(self.view.context_backtrace(pt))) 362 | else: 363 | stack = [] 364 | 365 | backtraces_text = [] 366 | backtraces_html = [] 367 | for i, ctx in enumerate(reversed(stack)): 368 | if SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4127: 369 | source_path = '{}:{}:{}'.format(ctx.source_file, *ctx.source_location) 370 | display_path = '{}:{}:{}'.format(os.path.splitext(ctx.source_file)[0], *ctx.source_location) 371 | if source_path.startswith('Packages/'): 372 | source_path = '${packages}/' + source_path[9:] 373 | display_path = display_path[9:] 374 | backtraces_text.append('{}. {} ({})'.format(i + 1, ctx.context_name, display_path)) 375 | backtraces_html.append("{} ({})".format( 376 | '%s' % ctx.context_name if ctx.context_name.startswith("anonymous ") else ctx.context_name, 377 | sublime.command_url('open_file', {'file': source_path, 'encoded_position': True}), 378 | display_path, 379 | )) 380 | elif SCOPE_CONTEXT_BACKTRACE_SUPPORT_v4087: 381 | backtraces_text.append(ctx) 382 | backtraces_html.append(ctx) 383 | 384 | if SCOPE_CONTEXT_BACKTRACE_SUPPORT and self.context_backtrace_info: 385 | self.scope_bfr.append(ENTRY.format(CONTEXT_BACKTRACE_KEY + ':', spacing.join(backtraces_text))) 386 | 387 | self.template_vars['context_backtrace'] = True 388 | self.template_vars["context_backtrace_stack"] = backtraces_html 389 | self.template_vars['context_backtrace_index'] = self.next_index() 390 | 391 | def get_appearance(self, color, bgcolor, style, source, line, col): 392 | """Get colors of foreground, background, and font styles.""" 393 | 394 | self.source = source 395 | self.line = line 396 | self.column = col 397 | 398 | self.scope_bfr.append(ENTRY.format(FG_KEY + ":", color)) 399 | self.scope_bfr.append(ENTRY.format(BG_KEY + ":", bgcolor)) 400 | self.scope_bfr.append(ENTRY.format(STYLE_KEY + ":", "normal" if not style else style)) 401 | 402 | self.template_vars['appearance'] = True 403 | self.get_color_box(color, 'fg', self.next_index()) 404 | self.get_color_box(bgcolor, 'bg', self.next_index()) 405 | 406 | style_label = set() 407 | style_open = [] 408 | style_close = [] 409 | 410 | for s in style.split(' '): 411 | if s == "bold": 412 | style_open.append('') 413 | style_close.insert(0, '') 414 | style_label.add('bold') 415 | elif s == "italic": 416 | style_open.append('') 417 | style_close.insert(0, '') 418 | style_label.add('italic') 419 | elif s == "underline": 420 | style_open.append('') 421 | style_close.insert(0, '') 422 | style_label.add('underline') 423 | elif s == "glow": 424 | style_open.append('') 425 | style_close.insert(0, '') 426 | style_label.add('glow') 427 | 428 | if len(style_label) == 0: 429 | style_label.add('normal') 430 | 431 | self.template_vars["style_open"] = ''.join(style_open) 432 | self.template_vars["style_close"] = ''.join(style_close) 433 | self.template_vars["style"] = ' '.join(list(style_label)) 434 | self.template_vars["style_index"] = self.next_index() 435 | 436 | def find_schemes(self): 437 | """Finc the syntax files.""" 438 | 439 | # Attempt syntax specific from view 440 | scheme_file = self.view.settings().get('color_scheme', None) 441 | 442 | # Get global scheme 443 | if scheme_file is None: 444 | pref_settings = sublime.load_settings('Preferences.sublime-settings') 445 | scheme_file = pref_settings.get('color_scheme') 446 | 447 | if scheme_file == 'auto' and AUTO: 448 | info = sublime.ui_info() 449 | scheme_file = info['color_scheme']['resolved_value'] 450 | 451 | scheme_file = scheme_file.replace('\\', '/') 452 | 453 | package_overrides = [] 454 | user_overrides = [] 455 | if scheme_file.endswith('.hidden-color-scheme'): 456 | pattern = '%s.hidden-color-scheme' 457 | else: 458 | pattern = '%s.sublime-color-scheme' 459 | 460 | for override in sublime.find_resources(pattern % os.path.basename(os.path.splitext(scheme_file)[0])): 461 | if override == scheme_file: 462 | continue 463 | if override.startswith('Packages/User/'): 464 | user_overrides.append(override) 465 | else: 466 | package_overrides.append(override) 467 | return scheme_file, package_overrides + user_overrides 468 | 469 | def get_scheme_syntax(self): 470 | """Get color scheme and syntax file path.""" 471 | 472 | self.scheme_file, self.overrides = self.find_schemes() 473 | self.syntax_file = self.view.settings().get('syntax') 474 | self.scope_bfr.append(ENTRY.format(SYNTAX_KEY + ":", self.syntax_file)) 475 | self.scope_bfr.append(ENTRY.format(SCHEME_KEY + ":", self.scheme_file)) 476 | text = [] 477 | for idx, override in enumerate(self.overrides, 1): 478 | text.append(ENTRY.format(OVERRIDE_SCHEME_KEY + (" {}:".format(idx)), override)) 479 | self.scope_bfr.append('\n'.join(text)) 480 | 481 | self.template_vars['files'] = True 482 | self.template_vars["syntax"] = self.syntax_file 483 | self.template_vars["syntax_index"] = self.next_index() 484 | self.template_vars["scheme"] = self.scheme_file 485 | self.template_vars["scheme_index"] = self.next_index() 486 | self.template_vars["overrides"] = self.overrides 487 | self.template_vars["overrides_index"] = self.next_index() 488 | 489 | def guess_style(self, scope, selected=False, no_bold=False, no_italic=False, explicit_background=False): 490 | """Guess color.""" 491 | 492 | # Remove leading '.' to account for old style CSS 493 | scope_style = self.view.style_for_scope(scope.lstrip('.')) 494 | style = {} 495 | style['foreground'] = scope_style['foreground'] 496 | style['background'] = scope_style.get('background') 497 | style['bold'] = scope_style.get('bold', False) and not no_bold 498 | style['italic'] = scope_style.get('italic', False) and not no_italic 499 | style['underline'] = scope_style.get('underline', False) 500 | style['glow'] = scope_style.get('glow', False) 501 | 502 | font_styles = [] 503 | for k, v in style.items(): 504 | if k in ('bold', 'italic', 'underline', 'glow'): 505 | if v is True: 506 | font_styles.append(k) 507 | font_styles = ' '.join(font_styles) 508 | 509 | defaults = self.view.style() 510 | if not explicit_background and not style.get('background'): 511 | style['background'] = defaults.get('background', '#FFFFFF') 512 | if selected: 513 | sfg = scope_style.get('selection_foreground', defaults.get('selection_foreground')) 514 | if sfg != '#00000000': 515 | style['foreground'] = sfg 516 | style['background'] = defaults.get('selection', '#0000FF') 517 | 518 | source = scope_style.get('source_file', '') 519 | line = '' 520 | col = '' 521 | if source: 522 | line = scope_style.get('source_line', '') 523 | col = scope_style.get('source_column', '') 524 | 525 | print("{}:{}:{}".format(source, line, col)) 526 | 527 | return SchemeColors(style['foreground'], style['background'], font_styles, source, line, col) 528 | 529 | def get_info(self, pt): 530 | """Get scope related info.""" 531 | 532 | scope = self.get_scope(pt) 533 | 534 | self.get_scope_context_backtrace(pt) 535 | 536 | if self.rowcol_info or self.points_info or self.highlight_extent: 537 | self.get_extents(pt) 538 | 539 | if (self.appearance_info): 540 | match = self.guess_style(scope) 541 | color = match.fg 542 | bgcolor = match.bg 543 | style = match.style 544 | if self.appearance_info: 545 | self.get_appearance(color, bgcolor, style, match.source, match.line, match.col) 546 | 547 | if self.file_path_info: 548 | self.get_scheme_syntax() 549 | 550 | self.scope_bfr_tool.append( 551 | mdpopups.md2html( 552 | self.view, 553 | self.popup_template, 554 | template_vars=self.template_vars, 555 | template_env_options={ 556 | "trim_blocks": True, 557 | "lstrip_blocks": True 558 | } 559 | ) 560 | ) 561 | 562 | def on_navigate(self, href): 563 | """Exceute link callback.""" 564 | 565 | params = href.split(':') 566 | key = params[0] 567 | index = int(params[1]) if len(params) > 1 else None 568 | if key == 'copy-all': 569 | sublime.set_clipboard('\n'.join(self.scope_bfr)) 570 | notify('Copied: All') 571 | elif key == 'copy-scope': 572 | copy_data( 573 | self.scope_bfr, 574 | SCOPE_KEY, 575 | index, 576 | lambda x: x.replace('\n' + ' ' * 31, ' ') 577 | ) 578 | elif key == 'copy-context-backtrace': 579 | copy_data( 580 | self.scope_bfr, 581 | CONTEXT_BACKTRACE_KEY, 582 | index, 583 | lambda x: x.replace('\n' + ' ' * 31, '\n') 584 | ) 585 | elif key == 'copy-points': 586 | copy_data(self.scope_bfr, PTS_KEY, index) 587 | elif key == 'copy-line-char': 588 | copy_data(self.scope_bfr, CHAR_LINE_KEY, index) 589 | elif key == 'copy-fg': 590 | copy_data(self.scope_bfr, FG_KEY, index) 591 | elif key == 'copy-bg': 592 | copy_data(self.scope_bfr, BG_KEY, index) 593 | elif key == 'copy-style': 594 | copy_data(self.scope_bfr, STYLE_KEY, index) 595 | elif key == 'copy-scheme': 596 | copy_data(self.scope_bfr, SCHEME_KEY, index) 597 | elif key == 'copy-syntax': 598 | copy_data(self.scope_bfr, SYNTAX_KEY, index) 599 | elif key == 'copy-overrides': 600 | copy_data( 601 | self.scope_bfr, 602 | "{} {}".format(OVERRIDE_SCHEME_KEY, params[2]), 603 | index, 604 | lambda text: self.overrides[int(params[2]) - 1] 605 | ) 606 | elif key == 'scheme' and self.scheme_file is not None: 607 | window = self.view.window() 608 | window.run_command( 609 | 'open_file', 610 | { 611 | "file": "${{packages}}/{}".format( 612 | self.scheme_file.replace( 613 | '\\', '/' 614 | ).replace('Packages/', '', 1) 615 | ) 616 | } 617 | ) 618 | elif key == 'source' and self.source is not None: 619 | window = self.view.window() 620 | file = sublime.expand_variables( 621 | "${{packages}}/{}:{}:{}".format( 622 | self.source.replace( 623 | '\\', '/' 624 | ).replace('Packages/', '', 1), 625 | self.line, 626 | self.column 627 | ), 628 | window.extract_variables() 629 | ) 630 | window.open_file( 631 | file, 632 | sublime.ENCODED_POSITION 633 | ) 634 | elif key == 'syntax' and self.syntax_file is not None: 635 | window = self.view.window() 636 | window.run_command( 637 | 'open_file', 638 | { 639 | "file": "${{packages}}/{}".format( 640 | self.syntax_file.replace( 641 | '\\', '/' 642 | ).replace('Packages/', '', 1) 643 | ) 644 | } 645 | ) 646 | elif key == 'override': 647 | window = self.view.window() 648 | window.run_command( 649 | 'open_file', 650 | { 651 | "file": "${{packages}}/{}".format( 652 | self.overrides[int(params[2]) - 1].replace('Packages/', '', 1) 653 | ) 654 | } 655 | ) 656 | 657 | def run(self, v): 658 | """Run ScopeHunter and display in the approriate way.""" 659 | 660 | self.view = v 661 | self.setup(sh_settings) 662 | 663 | self.window = self.view.window() 664 | self.scope_bfr = [] 665 | self.scope_bfr_tool = [] 666 | self.clips = [] 667 | self.popup_template = sublime.load_resource('Packages/ScopeHunter/popup.j2') 668 | self.scheme_file = None 669 | self.syntax_file = None 670 | self.show_popup = bool(sh_settings.get("show_popup", False)) 671 | self.clipboard = bool(sh_settings.get("clipboard", False)) 672 | self.multiselect = bool(sh_settings.get("multiselect", False)) 673 | self.highlight_extent = bool(sh_settings.get("highlight_extent", False)) 674 | self.highlight_scope = sh_settings.get("highlight_scope", 'invalid') 675 | self.highlight_style = sh_settings.get("highlight_style", 'outline') 676 | self.highlight_max_size = int(sh_settings.get("highlight_max_size", 100)) 677 | self.context_backtrace_info = bool(sh_settings.get("context_backtrace", False)) 678 | self.rowcol_info = bool(sh_settings.get("extent_line_char", False)) 679 | self.points_info = bool(sh_settings.get("extent_points", False)) 680 | self.appearance_info = bool(sh_settings.get("styling", False)) 681 | self.file_path_info = bool(sh_settings.get("file_paths", False)) 682 | self.scheme_info = self.appearance_info 683 | self.extents = [] 684 | 685 | # Get scope info for each selection wanted 686 | self.index = -1 687 | if len(self.view.sel()): 688 | if self.multiselect: 689 | count = 0 690 | for sel in self.view.sel(): 691 | if count > 0: 692 | self.scope_bfr_tool.append('\n
\n') 693 | self.init_template_vars() 694 | self.get_info(sel.b) 695 | count += 1 696 | else: 697 | self.init_template_vars() 698 | self.get_info(self.view.sel()[0].b) 699 | 700 | # Copy scopes to clipboard 701 | if self.clipboard: 702 | sublime.set_clipboard('\n'.join(self.clips)) 703 | 704 | if self.highlight_extent: 705 | style = extent_style(self.highlight_style) 706 | if style == 'underline': 707 | self.extents = underline(self.extents) 708 | self.view.add_regions( 709 | 'scope_hunter', 710 | self.extents, 711 | self.highlight_scope, 712 | '', 713 | style 714 | ) 715 | 716 | if self.scheme_info or self.rowcol_info or self.points_info or self.file_path_info: 717 | tail = mdpopups.md2html(self.view, COPY_ALL) 718 | else: 719 | tail = '' 720 | 721 | mdpopups.show_popup( 722 | self.view, 723 | ''.join(self.scope_bfr_tool) + tail, 724 | md=False, 725 | css=ADD_CSS, 726 | wrapper_class=('scope-hunter'), 727 | max_width=1000, on_navigate=self.on_navigate, 728 | ) 729 | 730 | 731 | get_selection_scopes = GetSelectionScope() 732 | 733 | 734 | class GetSelectionScopeCommand(sublime_plugin.TextCommand): 735 | """Command to get the selection(s) scope.""" 736 | 737 | def run(self, edit): 738 | """On demand scope request.""" 739 | 740 | sh_thread.modified = True 741 | 742 | def is_enabled(self): 743 | """Check if we should scope this view.""" 744 | 745 | return sh_thread.is_enabled(self.view) 746 | 747 | 748 | class ToggleSelectionScopeCommand(sublime_plugin.TextCommand): 749 | """Command to toggle instant scoper.""" 750 | 751 | def run(self, edit): 752 | """Enable or disable instant scoper.""" 753 | 754 | close_display = False 755 | 756 | sh_thread.instant_scoper = False 757 | if not self.view.settings().get('scope_hunter.view_enable', False): 758 | self.view.settings().set('scope_hunter.view_enable', True) 759 | sh_thread.modified = True 760 | sh_thread.time = time() 761 | else: 762 | self.view.settings().set('scope_hunter.view_enable', False) 763 | close_display = True 764 | 765 | if close_display: 766 | win = self.view.window() 767 | if win is not None: 768 | view = win.get_output_panel('scopehunter.results') 769 | parent_win = view.window() 770 | if parent_win: 771 | parent_win.run_command('hide_panel', {'cancel': True}) 772 | mdpopups.hide_popup(self.view) 773 | if ( 774 | self.view is not None and 775 | sh_thread.is_enabled(view) and 776 | bool(sh_settings.get("highlight_extent", False)) and 777 | len(view.get_regions("scope_hunter")) 778 | ): 779 | view.erase_regions("scope_hunter") 780 | 781 | 782 | class SelectionScopeListener(sublime_plugin.EventListener): 783 | """Listern for instant scoping.""" 784 | 785 | def clear_regions(self, view): 786 | """Clear the highlight regions.""" 787 | 788 | if ( 789 | bool(sh_settings.get("highlight_extent", False)) and 790 | len(view.get_regions("scope_hunter")) 791 | ): 792 | view.erase_regions("scope_hunter") 793 | 794 | def on_selection_modified(self, view): 795 | """Clean up regions or let thread know there was a modification.""" 796 | 797 | if sh_thread is None: 798 | return 799 | 800 | enabled = sh_thread.is_enabled(view) 801 | view_enable = view.settings().get('scope_hunter.view_enable', False) 802 | if (not sh_thread.instant_scoper and not view_enable) or not enabled: 803 | # clean up dirty highlights 804 | if enabled: 805 | self.clear_regions(view) 806 | else: 807 | sh_thread.modified = True 808 | sh_thread.time = time() 809 | 810 | 811 | class ShThread(threading.Thread): 812 | """Load up defaults.""" 813 | 814 | def __init__(self): 815 | """Setup the thread.""" 816 | self.reset() 817 | threading.Thread.__init__(self) 818 | 819 | def reset(self): 820 | """Reset the thread variables.""" 821 | self.wait_time = 0.12 822 | self.time = time() 823 | self.modified = False 824 | self.ignore_all = False 825 | self.instant_scoper = False 826 | self.abort = False 827 | 828 | def payload(self): 829 | """Code to run.""" 830 | # Ignore selection inside the routine 831 | self.modified = False 832 | self.ignore_all = True 833 | window = sublime.active_window() 834 | view = None if window is None else window.active_view() 835 | if view is not None: 836 | get_selection_scopes.run(view) 837 | self.ignore_all = False 838 | self.time = time() 839 | 840 | def is_enabled(self, view): 841 | """Check if we can execute.""" 842 | return not view.settings().get("is_widget") and not self.ignore_all 843 | 844 | def kill(self): 845 | """Kill thread.""" 846 | self.abort = True 847 | while self.is_alive(): 848 | pass 849 | self.reset() 850 | 851 | def run(self): 852 | """Thread loop.""" 853 | while not self.abort: 854 | if not self.ignore_all: 855 | if ( 856 | self.modified is True and 857 | time() - self.time > self.wait_time 858 | ): 859 | sublime.set_timeout(self.payload, 0) 860 | sleep(0.5) 861 | 862 | 863 | def init_plugin(): 864 | """Setup plugin variables and objects.""" 865 | 866 | global sh_thread 867 | global pref_settings 868 | global sh_settings 869 | 870 | # Preferences Settings 871 | pref_settings = sublime.load_settings('Preferences.sublime-settings') 872 | 873 | # Setup settings 874 | sh_settings = sublime.load_settings('scope_hunter.sublime-settings') 875 | 876 | # Setup thread 877 | if sh_thread is not None: 878 | # This shouldn't be needed, but just in case 879 | sh_thread.kill() 880 | sh_thread = ShThread() 881 | sh_thread.start() 882 | 883 | 884 | def plugin_loaded(): 885 | """Setup plugin.""" 886 | 887 | init_plugin() 888 | 889 | 890 | def plugin_unloaded(): 891 | """Kill the thread.""" 892 | 893 | sh_thread.kill() 894 | --------------------------------------------------------------------------------