├── tests ├── __init__.py ├── uniquely_named_file_ks27jArEz4 ├── test_package │ ├── .test_package_exists │ ├── helloworld.txt │ ├── directory │ │ └── goodbyeworld.txt │ └── UTF-8-test.txt ├── syntax_test_package │ ├── sublime_lib_test_no_name.sublime-syntax │ ├── sublime_lib_test_empty_name.sublime-syntax │ ├── sublime_lib_test_null_name.sublime-syntax │ ├── sublime_lib_test.sublime-syntax │ ├── sublime_lib_test_empty_name_tmLanguage.tmLanguage │ ├── sublime_lib_test.tmLanguage │ ├── sublime_lib_test_2.tmLanguage │ ├── sublime_lib_test.hidden-tmLanguage │ └── sublime_lib_test_2.hidden-tmLanguage ├── test_named_value.py ├── test_weak_method.py ├── test_yaml_util.py ├── temporary_package.py ├── test_encodings.py ├── test_panel.py ├── test_named_settings_dict.py ├── test_flags.py ├── test_collection_util.py ├── test_window_utils.py ├── test_output_panel.py ├── test_activity_indicator.py ├── test_syntax.py ├── test_region_manager.py ├── test_glob.py ├── test_view_utils.py ├── test_settings_dict.py ├── test_view_stream.py ├── test_selection_panel.py ├── test_pure_resource_path.py └── test_resource_path.py ├── .sublime-dependency ├── docs ├── source │ ├── _static │ │ ├── basic.css │ │ └── style.css │ ├── mocks │ │ └── sublime.py │ ├── extensions │ │ ├── strip_annotations.py │ │ └── better_toctree.py │ ├── index.rst │ └── conf.py └── Makefile ├── st3 ├── sublime_lib │ ├── _util │ │ ├── __init__.py │ │ ├── named_value.py │ │ ├── weak_method.py │ │ ├── guard.py │ │ ├── simple_yaml.py │ │ ├── glob.py │ │ ├── enum.py │ │ └── collections.py │ ├── _compat │ │ ├── __init__.py │ │ ├── enum.py │ │ ├── pathlib.py │ │ ├── typing.py │ │ └── typing_stubs.py │ ├── vendor │ │ ├── __init__.py │ │ ├── pathlib │ │ │ ├── __init__.py │ │ │ └── LICENSE.txt │ │ └── python │ │ │ ├── __init__.py │ │ │ ├── README │ │ │ ├── types.py │ │ │ └── LICENSE │ ├── __init__.py │ ├── region_manager.py │ ├── encodings.py │ ├── syntax.py │ ├── window_utils.py │ ├── activity_indicator.py │ ├── panel.py │ ├── show_selection_panel.py │ ├── view_utils.py │ ├── view_stream.py │ ├── settings_dict.py │ └── flags.py └── .pydocstyle ├── unittesting.json ├── .gitignore ├── .coveragerc ├── .flake8 ├── mypy.ini ├── .github └── workflows │ ├── ci.yml │ └── gh-pages.yml ├── LICENSE ├── README.md └── stubs └── sublime_plugin.pyi /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.sublime-dependency: -------------------------------------------------------------------------------- 1 | 01 2 | -------------------------------------------------------------------------------- /docs/source/_static/basic.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /st3/sublime_lib/_compat/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/uniquely_named_file_ks27jArEz4: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/pathlib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/python/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_package/.test_package_exists: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_package/helloworld.txt: -------------------------------------------------------------------------------- 1 | Hello, World! 2 | -------------------------------------------------------------------------------- /unittesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "deferred": true 3 | } 4 | -------------------------------------------------------------------------------- /tests/test_package/directory/goodbyeworld.txt: -------------------------------------------------------------------------------- 1 | Goodbye, World! 2 | -------------------------------------------------------------------------------- /st3/.pydocstyle: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | 3 | match_dir = ^(?!_) 4 | match = ^(?!_).*\.py 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .mypy_cache 3 | docs/html/ 4 | docs/source/modules/ 5 | *.doctree 6 | *.pickle 7 | modules.rst 8 | -------------------------------------------------------------------------------- /tests/test_package/UTF-8-test.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SublimeText/sublime_lib/HEAD/tests/test_package/UTF-8-test.txt -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = 3 | */tests/* 4 | */vendor/* 5 | */_compat/* 6 | 7 | [report] 8 | show_missing = True 9 | -------------------------------------------------------------------------------- /st3/sublime_lib/_compat/enum.py: -------------------------------------------------------------------------------- 1 | if False: # For MyPy only 2 | from enum import * # noqa: F401, F403 3 | else: 4 | from ..vendor.python.enum import * # type: ignore # noqa: F401, F403 5 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/named_value.py: -------------------------------------------------------------------------------- 1 | class NamedValue(): 2 | def __init__(self, name: str): 3 | self.name = name 4 | 5 | def __repr__(self) -> str: 6 | return self.name 7 | -------------------------------------------------------------------------------- /st3/sublime_lib/_compat/pathlib.py: -------------------------------------------------------------------------------- 1 | try: 2 | from pathlib import * # noqa: F401, F403 3 | except ImportError: 4 | from ..vendor.pathlib.pathlib import * # type: ignore # noqa: F401, F403 5 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_no_name.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | hidden: true 4 | file_extensions: 5 | - sublime_lib_test 6 | scope: source.sublime_lib_test 7 | contexts: 8 | main: [] 9 | -------------------------------------------------------------------------------- /st3/sublime_lib/_compat/typing.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 8, 0): 4 | from typing import * # noqa: F401, F403 5 | else: 6 | from .typing_stubs import * # type: ignore # noqa: F401, F403 7 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_empty_name.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | hidden: true 4 | name: '' 5 | file_extensions: 6 | - sublime_lib_test 7 | scope: source.sublime_lib_test 8 | contexts: 9 | main: [] 10 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_null_name.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | hidden: true 4 | name: null 5 | file_extensions: 6 | - sublime_lib_test 7 | scope: source.sublime_lib_test 8 | contexts: 9 | main: [] 10 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | hidden: true 4 | name: sublime_lib test syntax (sublime-syntax) 5 | file_extensions: 6 | - sublime_lib_test 7 | scope: source.sublime_lib_test 8 | contexts: 9 | main: [] 10 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | ignore = 4 | E731, # do not assign a lambda expression, use a def 5 | W503, # line break before binary operator 6 | 7 | exclude = 8 | .git, 9 | st3/sublime_lib/vendor, 10 | .venv, 11 | 12 | max-line-length = 99 13 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | disallow_untyped_defs = True 4 | mypy_path = 5 | st3, 6 | stubs, 7 | 8 | [mypy-sublime_lib.vendor.*] 9 | ignore_errors=True 10 | 11 | [mypy-sublime_lib._compat.typing_stubs] 12 | disallow_untyped_defs = False 13 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | 3 | # You can set these variables from the command line. 4 | SOURCEDIR = source 5 | BUILDDIR = . 6 | 7 | .PHONY: clean 8 | 9 | html: 10 | sphinx-build -M html "$(SOURCEDIR)" "$(BUILDDIR)" 11 | 12 | clean: 13 | rm -rf doctrees html 14 | -------------------------------------------------------------------------------- /tests/test_named_value.py: -------------------------------------------------------------------------------- 1 | from sublime_lib._util.named_value import NamedValue 2 | 3 | from unittest import TestCase 4 | 5 | 6 | class TestNamedValue(TestCase): 7 | 8 | def test_named_value(self): 9 | s = "Hello, World!" 10 | self.assertEqual( 11 | repr(NamedValue(s)), 12 | s 13 | ) 14 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/python/README: -------------------------------------------------------------------------------- 1 | The Python files in this directory are derived from the Python 3.6.5 source code. See the LICENSE file in this directory for licensing information. 2 | 3 | The files have been modified as follows: 4 | - A copyright notice has been added to each. 5 | - types.py is an incomplete excerpt of the original. 6 | - Imports have been modified as necessary. 7 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_empty_name_tmLanguage.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hidden 6 | 7 | 8 | name 9 | 10 | 11 | scopeName 12 | source.sublime_lib_test 13 | 14 | fileTypes 15 | 16 | sublime_lib_test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hidden 6 | 7 | 8 | name 9 | sublime_lib test syntax (tmLanguage) 10 | 11 | scopeName 12 | source.sublime_lib_test 13 | 14 | fileTypes 15 | 16 | sublime_lib_test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_2.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hidden 6 | 7 | 8 | name 9 | sublime_lib test syntax 2 (tmLanguage) 10 | 11 | scopeName 12 | source.sublime_lib_test_2 13 | 14 | fileTypes 15 | 16 | sublime_lib_test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hidden 6 | 7 | 8 | name 9 | sublime_lib test syntax (hidden-tmLanguage) 10 | 11 | scopeName 12 | source.sublime_lib_test 13 | 14 | fileTypes 15 | 16 | sublime_lib_test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /tests/syntax_test_package/sublime_lib_test_2.hidden-tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | hidden 6 | 7 | 8 | name 9 | sublime_lib test syntax 2 (hidden-tmLanguage) 10 | 11 | scopeName 12 | source.sublime_lib_test_2 13 | 14 | fileTypes 15 | 16 | sublime_lib_test 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/weak_method.py: -------------------------------------------------------------------------------- 1 | import weakref 2 | 3 | from .._compat.typing import Callable, Any 4 | from types import MethodType 5 | 6 | 7 | __all__ = ['weak_method'] 8 | 9 | 10 | def weak_method(method: Callable) -> Callable: 11 | assert isinstance(method, MethodType) 12 | self_ref = weakref.ref(method.__self__) 13 | function_ref = weakref.ref(method.__func__) 14 | 15 | def wrapped(*args: Any, **kwargs: Any) -> Any: 16 | self = self_ref() 17 | function = function_ref() 18 | if self is not None and function is not None: 19 | return function(self, *args, **kwargs) 20 | 21 | return wrapped 22 | -------------------------------------------------------------------------------- /tests/test_weak_method.py: -------------------------------------------------------------------------------- 1 | from sublime_lib._util.weak_method import weak_method 2 | 3 | from unittest import TestCase 4 | 5 | 6 | class TestWeakMethod(TestCase): 7 | 8 | def test_weak(self): 9 | count = 0 10 | 11 | class TestObject: 12 | def foo(self): 13 | nonlocal count 14 | count += 1 15 | 16 | obj = TestObject() 17 | 18 | obj.foo() 19 | self.assertEqual(count, 1) 20 | 21 | weak = weak_method(obj.foo) 22 | 23 | weak() 24 | self.assertEqual(count, 2) 25 | 26 | del obj 27 | 28 | weak() 29 | self.assertEqual(count, 2) 30 | -------------------------------------------------------------------------------- /st3/sublime_lib/__init__.py: -------------------------------------------------------------------------------- 1 | from .panel import Panel, OutputPanel # noqa: F401 2 | from .resource_path import ResourcePath # noqa: F401 3 | from .show_selection_panel import show_selection_panel, NO_SELECTION # noqa: F401 4 | from .settings_dict import SettingsDict, NamedSettingsDict # noqa: F401 5 | from .syntax import list_syntaxes, get_syntax_for_scope # noqa: F401 6 | from .view_stream import ViewStream # noqa: F401 7 | from .view_utils import new_view, close_view, LineEnding # noqa: F401 8 | from .activity_indicator import ActivityIndicator # noqa: F401 9 | from .window_utils import new_window, close_window # noqa: F401 10 | from .region_manager import RegionManager # noqa: F401 11 | -------------------------------------------------------------------------------- /docs/source/mocks/sublime.py: -------------------------------------------------------------------------------- 1 | from sphinx.ext.autodoc.mock import _MockObject 2 | 3 | import sys 4 | 5 | 6 | class MockType(_MockObject): 7 | def __init__(self, name): 8 | self.name = name 9 | 10 | def __repr__(self): 11 | return self.name 12 | 13 | 14 | class SublimeMock: 15 | Region = MockType('sublime.Region') 16 | View = MockType('sublime.View') 17 | Window = MockType('sublime.Window') 18 | Settings = MockType('sublime.Settings') 19 | 20 | def __getattr__(self, key): 21 | if key.isupper(): 22 | return hash(key) 23 | else: 24 | raise AttributeError(key) 25 | 26 | 27 | sys.modules[__name__] = SublimeMock() 28 | -------------------------------------------------------------------------------- /tests/test_yaml_util.py: -------------------------------------------------------------------------------- 1 | from sublime_lib._util.simple_yaml import parse_simple_top_level_keys 2 | 3 | from unittest import TestCase 4 | 5 | 6 | TEXT = r""" 7 | # A comment 8 | 'single': 'test '' value' 9 | "double": "test\nvalue" 10 | unquoted: test\value 11 | true: true 12 | false: false 13 | null: null 14 | multiline: 15 | not: parsed 16 | """ 17 | 18 | 19 | class TestSettingsDict(TestCase): 20 | 21 | def test_parse_simple_top_level_keys(self): 22 | self.assertEqual( 23 | parse_simple_top_level_keys(TEXT), 24 | { 25 | 'single': 'test \' value', 26 | 'double': 'test\nvalue', 27 | 'unquoted': 'test\\value', 28 | True: True, 29 | False: False, 30 | None: None, 31 | } 32 | ) 33 | -------------------------------------------------------------------------------- /docs/source/extensions/strip_annotations.py: -------------------------------------------------------------------------------- 1 | from sphinx.util.inspect import stringify_signature 2 | 3 | import inspect 4 | 5 | 6 | __all__ = ['strip_annotations'] 7 | 8 | 9 | def strip_annotations( 10 | app, 11 | what: str, 12 | name: str, 13 | obj, 14 | options, 15 | signature, 16 | return_annotation 17 | ): 18 | if what not in {'function', 'method', 'class'}: 19 | return 20 | 21 | original_signature = inspect.signature(obj) 22 | new_signature = original_signature.replace( 23 | return_annotation=inspect.Signature.empty, 24 | parameters=[ 25 | param.replace(annotation=inspect.Parameter.empty) 26 | for param in original_signature.parameters.values() 27 | if param.name != 'self' 28 | ], 29 | ) 30 | 31 | return stringify_signature(new_signature), None 32 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/guard.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from .._compat.typing import Any, Callable, ContextManager, Optional, TypeVar 4 | 5 | 6 | _Self = TypeVar('_Self') 7 | _R = TypeVar('_R') 8 | _WrappedType = Callable[..., _R] 9 | 10 | 11 | def define_guard( 12 | guard_fn: Callable[[_Self], Optional[ContextManager]] 13 | ) -> Callable[[_WrappedType], _WrappedType]: 14 | def decorator(wrapped: _WrappedType) -> _WrappedType: 15 | @wraps(wrapped) 16 | def wrapper_guards(self: _Self, *args: Any, **kwargs: Any) -> _R: 17 | ret_val = guard_fn(self) 18 | if ret_val is not None: 19 | with ret_val: 20 | return wrapped(self, *args, **kwargs) 21 | else: 22 | return wrapped(self, *args, **kwargs) 23 | 24 | return wrapper_guards 25 | 26 | return decorator 27 | -------------------------------------------------------------------------------- /tests/temporary_package.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from sublime_lib import ResourcePath 3 | 4 | 5 | __all__ = ['TemporaryPackage'] 6 | 7 | 8 | class TemporaryPackage: 9 | def __init__(self, package_name: str, resource_path: ResourcePath) -> None: 10 | self.package_name = package_name 11 | self.resource_path = resource_path 12 | 13 | @property 14 | def package_path(self) -> ResourcePath: 15 | return ResourcePath("Packages") / self.package_name 16 | 17 | def create(self) -> None: 18 | shutil.copytree( 19 | src=str(self.resource_path.file_path()), 20 | dst=str(self.package_path.file_path()), 21 | ) 22 | 23 | def destroy(self) -> None: 24 | shutil.rmtree( 25 | str(self.package_path.file_path()), 26 | ignore_errors=True 27 | ) 28 | 29 | def exists(self) -> bool: 30 | return len(self.package_path.rglob('*')) > 0 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | flake8: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | 11 | - name: Setup Python 12 | uses: actions/setup-python@v1 13 | with: 14 | python-version: 3.x 15 | 16 | - name: Install flake8 17 | run: | 18 | python -m pip install -U pip 19 | pip install flake8 20 | 21 | - run: flake8 . 22 | 23 | pydocstyle: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v1 27 | 28 | - name: Setup Python 29 | uses: actions/setup-python@v1 30 | with: 31 | python-version: 3.x 32 | 33 | - name: Install pydocstyle 34 | run: | 35 | python -m pip install -U pip 36 | pip install pydocstyle 37 | 38 | - name: Run pydocstyle (allow failure) 39 | run: pydocstyle 40 | working-directory: st3/sublime_lib 41 | continue-on-error: true 42 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/simple_yaml.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import re 3 | 4 | from .._compat.typing import Dict, Union 5 | 6 | YamlScalar = Union[str, bool, None] 7 | 8 | __all__ = ['parse_simple_top_level_keys'] 9 | 10 | 11 | def parse_simple_top_level_keys(text: str) -> Dict[YamlScalar, YamlScalar]: 12 | return { 13 | _parse_yaml_value(match.group(1)): 14 | _parse_yaml_value(match.group(2)) 15 | for match in re.finditer(r'(?m)^([^\s#].*?\s*): *(.+) *$', text) 16 | } 17 | 18 | 19 | def _parse_yaml_value(value: str) -> YamlScalar: 20 | if value.startswith("'"): 21 | return value[1:-1].replace("''", "'") 22 | elif value.startswith('"'): 23 | # JSON and YAML quotation rules are very similar, if not identical 24 | return sublime.decode_value(value) 25 | elif value == "true": 26 | return True 27 | elif value == "false": 28 | return False 29 | elif value == "null": 30 | return None 31 | else: 32 | # Does not handle numbers because we don't expect any 33 | return value 34 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Github Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | 14 | - name: Setup Python 15 | uses: actions/setup-python@v1 16 | with: 17 | python-version: '3.x' 18 | 19 | - name: Install Dependencies 20 | run: | 21 | python -m pip install -U pip 22 | python -m pip install -U sphinx 23 | python -m pip install -U sphinxcontrib.prettyspecialmethods 24 | 25 | - name: Build Docs 26 | run: make -C docs clean html 27 | ## https://github.com/marketplace/actions/sphinx-build 28 | # uses: ammaraskar/sphinx-action@0.3 29 | # with: 30 | # docs-folder: docs/ 31 | # repo-token: ${{ secrets.GITHUB_TOKEN }} 32 | # build-command: "make clean html" 33 | 34 | - name: Deploy Docs 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 38 | publish_dir: docs/html 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Smith 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/pathlib/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2014 Antoine Pitrou and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /tests/test_encodings.py: -------------------------------------------------------------------------------- 1 | from sublime_lib.encodings import from_sublime, to_sublime 2 | 3 | from unittest import TestCase 4 | 5 | 6 | class TestEncodings(TestCase): 7 | 8 | def test_from(self): 9 | self.assertEqual( 10 | from_sublime("Western (Windows 1252)"), 11 | "cp1252" 12 | ) 13 | 14 | def test_from_error(self): 15 | with self.assertRaises(ValueError): 16 | from_sublime("Nonexistent") 17 | 18 | def test_to(self): 19 | self.assertEqual( 20 | to_sublime("cp1252"), 21 | "Western (Windows 1252)" 22 | ) 23 | 24 | def test_to_with_aliases(self): 25 | self.assertEqual( 26 | to_sublime("mac-latin2"), 27 | "Central European (Mac)" 28 | ) 29 | self.assertEqual( 30 | to_sublime("mac_latin2"), 31 | "Central European (Mac)" 32 | ) 33 | 34 | self.assertEqual( 35 | to_sublime(from_sublime( 36 | "Central European (Mac)" 37 | )), 38 | "Central European (Mac)" 39 | ) 40 | 41 | def test_to_error(self): 42 | with self.assertRaises(ValueError): 43 | to_sublime("Nonexistent") 44 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/glob.py: -------------------------------------------------------------------------------- 1 | import re 2 | from functools import lru_cache 3 | 4 | from .._compat.typing import Callable 5 | 6 | __all__ = ['get_glob_matcher'] 7 | 8 | 9 | GLOB_RE = re.compile(r"""(?x)( 10 | \* 11 | | \? 12 | | \[ .*? \] 13 | )""") 14 | 15 | 16 | @lru_cache() 17 | def get_glob_matcher(pattern: str) -> Callable[[str], bool]: 18 | if pattern.startswith('/'): 19 | pattern = pattern[1:] 20 | else: 21 | pattern = '**/' + pattern 22 | 23 | expr_string = r'\A' 24 | for component in pattern.split('/'): 25 | if component == '': 26 | pass 27 | elif component == '*': 28 | # Component must not be empty. 29 | expr_string += r'(?:[^/])+' + '/' 30 | elif component == '**': 31 | expr_string += r'(?:.*(?:\Z|/))?' 32 | elif '**' in component: 33 | raise ValueError("Invalid pattern: '**' can only be an entire path component") 34 | else: 35 | for part in GLOB_RE.split(component): 36 | if part == '': 37 | pass 38 | elif part == '*': 39 | expr_string += r'(?:[^/])*' 40 | elif part == '?': 41 | expr_string += r'(?:[^/])' 42 | elif part[0] == '[': 43 | expr_string += part 44 | else: 45 | expr_string += re.escape(part) 46 | expr_string += '/' 47 | 48 | expr_string = expr_string.rstrip('/') + r'\Z' 49 | expr = re.compile(expr_string) 50 | 51 | return lambda path: (expr.search(path) is not None) 52 | -------------------------------------------------------------------------------- /tests/test_panel.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import Panel 3 | 4 | from unittest import TestCase 5 | 6 | 7 | class TestOutputPanel(TestCase): 8 | 9 | def setUp(self): 10 | self.window = sublime.active_window() 11 | self.panel_to_restore = self.window.active_panel() 12 | 13 | def tearDown(self): 14 | if self.panel_to_restore: 15 | self.window.run_command("show_panel", {"panel": self.panel_to_restore}) 16 | 17 | def test_exists(self): 18 | panel = Panel(self.window, "console") 19 | self.assertIn("console", self.window.panels()) 20 | self.assertTrue(panel.exists()) 21 | 22 | def test_not_exists(self): 23 | with self.assertRaises(ValueError): 24 | Panel(self.window, "nonexistent_panel") 25 | 26 | def test_show_hide(self): 27 | panel = Panel(self.window, "console") 28 | 29 | panel.show() 30 | self.assertTrue(panel.is_visible()) 31 | self.assertEqual(self.window.active_panel(), "console") 32 | 33 | panel.hide() 34 | self.assertFalse(panel.is_visible()) 35 | self.assertNotEqual(self.window.active_panel(), "console") 36 | 37 | panel.show() 38 | self.assertTrue(panel.is_visible()) 39 | self.assertEqual(self.window.active_panel(), "console") 40 | 41 | panel.toggle_visibility() 42 | self.assertFalse(panel.is_visible()) 43 | self.assertNotEqual(self.window.active_panel(), "console") 44 | 45 | panel.toggle_visibility() 46 | self.assertTrue(panel.is_visible()) 47 | self.assertEqual(self.window.active_panel(), "console") 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sublime_lib 2 | 3 | A utility library for Sublime Text providing a variety of convenience features for other packages to use. 4 | 5 | ## Installation 6 | 7 | To make use of sublime_lib in your own package, first declare it as a [dependency](https://packagecontrol.io/docs/dependencies) of your package. Create a file named `dependencies.json` in the root of your package with the following contents: 8 | 9 | ```json 10 | { 11 | "*": { 12 | "*": [ 13 | "sublime_lib" 14 | ] 15 | } 16 | } 17 | ``` 18 | 19 | Once you have declared the dependency, open the command palette and run `Package Control: Satisfy Dependencies` to ensure that sublime_lib is installed and available for use. 20 | 21 | Then, anywhere in your package, you can import sublime_lib by name: 22 | 23 | ```python 24 | import sublime_lib 25 | ``` 26 | 27 | ## Features 28 | 29 | For complete documentation of all features, see the [API documentation](https://sublimetext.github.io/sublime_lib/). 30 | 31 | Highlights include: 32 | 33 | - [`SettingsDict`](https://sublimetext.github.io/sublime_lib/modules/sublime_lib.settings_dict.html), which wraps a `sublime.Settings` object with an interface modeled after a standard Python `dict`. 34 | - [`ViewStream`](https://sublimetext.github.io/sublime_lib/modules/sublime_lib.view_stream.html), a standard [Python IO stream](https://docs.python.org/3/library/io.html#io.TextIOBase) wrapping a `sublime.View` object; and [OutputPanel](https://sublimetext.github.io/sublime_lib/modules/sublime_lib.output_panel.html), which extends `ViewStream` to provide additional functionality for output panel views. 35 | - The [`syntax` submodule](https://sublimetext.github.io/sublime_lib/modules/sublime_lib.syntax.html), providing methods to list all loaded syntax definitions and to find a syntax matching a given scope. 36 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/enum.py: -------------------------------------------------------------------------------- 1 | from functools import partial 2 | 3 | from .._compat.enum import EnumMeta, Enum, Flag 4 | from .._compat.typing import Any, Callable, Optional 5 | 6 | 7 | __all__ = ['ExtensibleConstructorMeta', 'construct_with_alternatives', 'construct_union'] 8 | 9 | 10 | class ExtensibleConstructorMeta(EnumMeta): 11 | def __call__(cls, *args: Any, **kwargs: Any) -> Any: 12 | return cls.__new__(cls, *args, **kwargs) # type: ignore 13 | 14 | 15 | def extend_constructor( 16 | constructor: Callable[..., Enum] 17 | ) -> Callable[[EnumMeta], EnumMeta]: 18 | def decorator(cls: EnumMeta) -> EnumMeta: 19 | next_constructor = partial(cls.__new__, cls) 20 | 21 | def __new__(cls: EnumMeta, *args: Any, **kwargs: Any) -> Enum: 22 | return constructor(next_constructor, cls, *args, **kwargs) 23 | 24 | cls.__new__ = __new__ # type: ignore 25 | return cls 26 | 27 | return decorator 28 | 29 | 30 | def construct_with_alternatives( 31 | provider: Callable[..., Optional[Enum]] 32 | ) -> Callable[[EnumMeta], EnumMeta]: 33 | def constructor(next_constructor: Callable[..., Enum], cls: EnumMeta, 34 | *args: Any, **kwargs: Any) -> Enum: 35 | try: 36 | return next_constructor(*args, **kwargs) 37 | except ValueError: 38 | result = provider(cls, *args, **kwargs) 39 | if result is None: 40 | raise 41 | else: 42 | return result 43 | 44 | return extend_constructor(constructor) 45 | 46 | 47 | def _construct_union( 48 | next_constructor: Callable[[Any], Flag], 49 | cls: ExtensibleConstructorMeta, 50 | *args: Any 51 | ) -> Any: 52 | if args: 53 | ret, *rest = iter(next_constructor(arg) for arg in args) 54 | for value in rest: 55 | ret |= value 56 | 57 | return ret 58 | else: 59 | return next_constructor(0) 60 | 61 | 62 | construct_union = extend_constructor(_construct_union) 63 | -------------------------------------------------------------------------------- /tests/test_named_settings_dict.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import NamedSettingsDict, SettingsDict 3 | 4 | import os 5 | from os import path 6 | 7 | from unittesting import DeferrableTestCase 8 | 9 | 10 | class TestNamedSettingsDict(DeferrableTestCase): 11 | def setUp(self): 12 | self.name = "_sublime_lib_NamedSettingsDictTest" 13 | self.fancy = NamedSettingsDict(self.name) 14 | self.settings_path = path.join(sublime.packages_path(), 'User', self.fancy.file_name) 15 | 16 | def tearDown(self): 17 | try: 18 | os.remove(self.settings_path) 19 | except FileNotFoundError: 20 | pass 21 | 22 | def test_named(self): 23 | other = NamedSettingsDict(self.name) 24 | 25 | self.fancy.pop("example_setting", None) 26 | self.assertNotIn("example_setting", self.fancy) 27 | self.fancy["example_setting"] = "Hello, World!" 28 | 29 | self.assertIn("example_setting", self.fancy) 30 | self.assertIn("example_setting", other) 31 | 32 | self.fancy.save() 33 | 34 | yield 35 | 36 | self.assertTrue(path.exists(self.settings_path)) 37 | 38 | def test_file_extension(self): 39 | other = NamedSettingsDict(self.name + '.sublime-settings') 40 | 41 | self.fancy.pop("example_setting", None) 42 | self.assertNotIn("example_setting", self.fancy) 43 | self.fancy["example_setting"] = "Hello, World!" 44 | self.assertEquals(other['example_setting'], 'Hello, World!') 45 | 46 | def test_equal(self): 47 | other = NamedSettingsDict(self.name + '.sublime-settings') 48 | self.assertEqual(self.fancy, other) 49 | 50 | def test_not_equal(self): 51 | other = NamedSettingsDict('Preferences.sublime-settings') 52 | self.assertNotEqual(self.fancy, other) 53 | 54 | def test_not_equal_unnamed(self): 55 | other = SettingsDict(self.fancy.settings) 56 | self.assertNotEqual(self.fancy, other) 57 | self.assertNotEqual(other, self.fancy) 58 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | :mod:`sublime_lib` API Reference 2 | ================================ 3 | 4 | A utility library for Sublime Text providing a variety of convenience features for other packages to use. 5 | 6 | For general documentation, see the `README `_. 7 | 8 | Most :mod:`sublime_lib` classes and functions rely on 9 | the `Sublime Text API `_. 10 | As a result, :mod:`sublime_lib` functionality should not be used at import time. 11 | Instead, any initialization should be performed in :func:`plugin_loaded`. 12 | 13 | Settings dictionaries 14 | --------------------- 15 | 16 | .. autoclass:: sublime_lib.SettingsDict 17 | .. autoclass:: sublime_lib.NamedSettingsDict 18 | 19 | Output streams and panels 20 | ------------------------- 21 | 22 | .. autoclass:: sublime_lib.ViewStream 23 | .. autoclass:: sublime_lib.Panel 24 | .. autoclass:: sublime_lib.OutputPanel 25 | 26 | Resource paths 27 | -------------- 28 | 29 | .. autoclass:: sublime_lib.ResourcePath 30 | 31 | View utilities 32 | -------------- 33 | 34 | .. autofunction:: sublime_lib.new_view 35 | .. autofunction:: sublime_lib.close_view 36 | .. autoclass:: sublime_lib.LineEnding 37 | 38 | Window utilities 39 | ---------------- 40 | 41 | .. autofunction:: sublime_lib.new_window 42 | .. autofunction:: sublime_lib.close_window 43 | .. autofunction:: sublime_lib.show_selection_panel 44 | 45 | Syntax utilities 46 | ---------------- 47 | 48 | .. autofunction:: sublime_lib.list_syntaxes 49 | .. autofunction:: sublime_lib.get_syntax_for_scope 50 | 51 | Activity indicator 52 | ------------------ 53 | 54 | .. autoclass:: sublime_lib.ActivityIndicator 55 | 56 | Region manager 57 | ------------------ 58 | 59 | .. autoclass:: sublime_lib.RegionManager 60 | 61 | :mod:`~sublime_lib.encodings` submodule 62 | --------------------------------------- 63 | 64 | .. automodule:: sublime_lib.encodings 65 | 66 | :mod:`~sublime_lib.flags` submodule 67 | ----------------------------------- 68 | 69 | .. automodule:: sublime_lib.flags 70 | -------------------------------------------------------------------------------- /st3/sublime_lib/_util/collections.py: -------------------------------------------------------------------------------- 1 | from collections.abc import Mapping, Sequence 2 | 3 | from .._compat.typing import Callable, Dict, Iterable, TypeVar, Union 4 | 5 | 6 | _V = TypeVar('_V') 7 | _Result = TypeVar('_Result') 8 | _Default = TypeVar('_Default') 9 | _Value = Union[bool, int, float, str, list, dict, None] 10 | 11 | 12 | __all__ = ['projection', 'get_selector', 'isiterable', 'ismapping', 'is_sequence_not_str'] 13 | 14 | 15 | def projection( 16 | d: Dict[str, _V], 17 | keys: Union[Dict[str, str], Iterable[str]] 18 | ) -> Dict[str, _V]: 19 | """ 20 | Return a new :class:`dict` with keys of ``d`` restricted to values in ``keys``. 21 | 22 | .. code-block:: python 23 | 24 | >>> projection({'a': 1, 'b': 2}, ['b']) 25 | {'b': 2} 26 | 27 | If ``keys`` is a :class:`dict`, then it maps keys of the original dict to 28 | keys of the result: 29 | 30 | .. code-block:: python 31 | 32 | >>> projection({'a': 1, 'b': 2}, {'b': 'c'}) 33 | {'c': 2} 34 | """ 35 | if isinstance(keys, dict): 36 | return { 37 | new_key: d[original_key] 38 | for original_key, new_key in keys.items() 39 | if original_key in d 40 | } 41 | else: 42 | return { 43 | key: d[key] 44 | for key in keys 45 | if key in d 46 | } 47 | 48 | 49 | def get_selector(selector: object, default_value: object = None) -> Callable: # noqa: F811 50 | if callable(selector): 51 | return selector 52 | elif isinstance(selector, str): 53 | return lambda this: this.get(selector, default_value) 54 | elif isiterable(selector): 55 | return lambda this: projection(this, selector) # type: ignore 56 | else: 57 | raise TypeError( 58 | 'The selector should be a function, string, or iterable of strings.' 59 | ) 60 | 61 | 62 | def isiterable(obj: object) -> bool: 63 | try: 64 | iter(obj) # type: ignore 65 | return True 66 | except TypeError: 67 | return False 68 | 69 | 70 | def ismapping(obj: object) -> bool: 71 | return isinstance(obj, Mapping) 72 | 73 | 74 | def is_sequence_not_str(obj: object) -> bool: 75 | return isinstance(obj, Sequence) and not isinstance(obj, str) 76 | -------------------------------------------------------------------------------- /tests/test_flags.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_lib.flags as flags 3 | 4 | from sublime_lib.vendor.python.enum import IntFlag 5 | 6 | from functools import reduce 7 | from unittest import TestCase 8 | 9 | 10 | class TestFlags(TestCase): 11 | 12 | def _test_enum(self, enum, prefix=''): 13 | for item in enum: 14 | self.assertEqual(item, getattr(sublime, prefix + item.name)) 15 | self.assertEqual(item, enum(item.name)) 16 | 17 | if issubclass(enum, IntFlag): 18 | self.assertEqual( 19 | enum(*[item.name for item in enum]), 20 | reduce(lambda a, b: a | b, enum) 21 | ) 22 | 23 | def test_flags(self): 24 | self._test_enum(flags.DialogResult, 'DIALOG_') 25 | self._test_enum(flags.PointClass, 'CLASS_') 26 | self._test_enum(flags.PhantomLayout, 'LAYOUT_') 27 | self._test_enum(flags.HoverLocation, 'HOVER_') 28 | self._test_enum(flags.QueryContextOperator, 'OP_') 29 | 30 | self._test_enum(flags.FindOption) 31 | self._test_enum(flags.RegionOption) 32 | self._test_enum(flags.PopupOption) 33 | self._test_enum(flags.OpenFileOption) 34 | self._test_enum(flags.QuickPanelOption) 35 | self._test_enum(flags.CompletionOptions) 36 | 37 | def test_from_strings(self): 38 | self.assertEqual( 39 | flags.RegionOption('DRAW_EMPTY', 'HIDE_ON_MINIMAP'), 40 | flags.RegionOption.DRAW_EMPTY | flags.RegionOption.HIDE_ON_MINIMAP 41 | ) 42 | 43 | def test_from_strings_empty(self): 44 | self.assertEqual( 45 | flags.RegionOption(), 46 | flags.RegionOption(0) 47 | ) 48 | 49 | def test_query_context_operators(self): 50 | ops = flags.QueryContextOperator 51 | 52 | tests = [ 53 | ('EQUAL', 'x', 'x', 'y'), 54 | ('REGEX_MATCH', 'aaa', r'a+', r'a'), 55 | ('REGEX_CONTAINS', 'aaa', r'a', r'b'), 56 | ] 57 | 58 | for op, key, success, failure in tests: 59 | self.assertTrue(ops(op).apply(key, success)) 60 | self.assertFalse(ops(op).apply(key, failure)) 61 | 62 | self.assertTrue(ops('NOT_' + op).apply(key, failure)) 63 | self.assertFalse(ops('NOT_' + op).apply(key, success)) 64 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/python/types.py: -------------------------------------------------------------------------------- 1 | # Copyright © 2001-2018 Python Software Foundation; All Rights Reserved 2 | 3 | class DynamicClassAttribute: 4 | """Route attribute access on a class to __getattr__. 5 | 6 | This is a descriptor, used to define attributes that act differently when 7 | accessed through an instance and through a class. Instance access remains 8 | normal, but access to an attribute through a class will be routed to the 9 | class's __getattr__ method; this is done by raising AttributeError. 10 | 11 | This allows one to have properties active on an instance, and have virtual 12 | attributes on the class with the same name (see Enum for an example). 13 | 14 | """ 15 | def __init__(self, fget=None, fset=None, fdel=None, doc=None): 16 | self.fget = fget 17 | self.fset = fset 18 | self.fdel = fdel 19 | # next two lines make DynamicClassAttribute act the same as property 20 | self.__doc__ = doc or fget.__doc__ 21 | self.overwrite_doc = doc is None 22 | # support for abstract methods 23 | self.__isabstractmethod__ = bool(getattr(fget, '__isabstractmethod__', False)) 24 | 25 | def __get__(self, instance, ownerclass=None): 26 | if instance is None: 27 | if self.__isabstractmethod__: 28 | return self 29 | raise AttributeError() 30 | elif self.fget is None: 31 | raise AttributeError("unreadable attribute") 32 | return self.fget(instance) 33 | 34 | def __set__(self, instance, value): 35 | if self.fset is None: 36 | raise AttributeError("can't set attribute") 37 | self.fset(instance, value) 38 | 39 | def __delete__(self, instance): 40 | if self.fdel is None: 41 | raise AttributeError("can't delete attribute") 42 | self.fdel(instance) 43 | 44 | def getter(self, fget): 45 | fdoc = fget.__doc__ if self.overwrite_doc else None 46 | result = type(self)(fget, self.fset, self.fdel, fdoc or self.__doc__) 47 | result.overwrite_doc = self.overwrite_doc 48 | return result 49 | 50 | def setter(self, fset): 51 | result = type(self)(self.fget, fset, self.fdel, self.__doc__) 52 | result.overwrite_doc = self.overwrite_doc 53 | return result 54 | 55 | def deleter(self, fdel): 56 | result = type(self)(self.fget, self.fset, fdel, self.__doc__) 57 | result.overwrite_doc = self.overwrite_doc 58 | return result 59 | -------------------------------------------------------------------------------- /st3/sublime_lib/region_manager.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from ._compat.typing import Optional, List, TypeVar, Collection 4 | from .flags import RegionOption 5 | 6 | 7 | __all__ = ['RegionManager'] 8 | 9 | 10 | T = TypeVar('T') 11 | 12 | 13 | def _coalesce(*values: Optional[T]) -> T: 14 | return next(value for value in values if value is not None) 15 | 16 | 17 | class RegionManager: 18 | """A manager for regions in a given :class:`sublime.View` with the same `key`. 19 | 20 | If `key` is not given, 21 | a unique identifier will be used. 22 | 23 | If the `scope`, `icon`, and `flags` args are given, 24 | then they will be used as default values for :meth:`set`. 25 | 26 | When the region manager is garbage-collected, all managed regions will be erased. 27 | 28 | .. versionadded:: 1.4 29 | """ 30 | 31 | def __init__( 32 | self, 33 | view: sublime.View, 34 | key: Optional[str] = None, 35 | *, 36 | scope: Optional[str] = None, 37 | icon: Optional[str] = None, 38 | flags: Optional[RegionOption] = None 39 | ): 40 | self.view = view 41 | 42 | if key is None: 43 | self.key = str(id(self)) 44 | else: 45 | self.key = key 46 | 47 | self.scope = scope 48 | self.icon = icon 49 | self.flags = flags 50 | 51 | def __del__(self) -> None: 52 | self.erase() 53 | 54 | def set( 55 | self, 56 | regions: Collection[sublime.Region], 57 | *, 58 | scope: Optional[str] = None, 59 | icon: Optional[str] = None, 60 | flags: Optional[RegionOption] = None 61 | ) -> None: 62 | """Replace managed regions with the given regions. 63 | 64 | If the `scope`, `icon`, and `flags` arguments are given, 65 | then they will be passed to :meth:`sublime.add_regions`. 66 | Otherwise, the defaults specified in the initializer will be used. 67 | """ 68 | self.view.add_regions( 69 | self.key, 70 | regions, 71 | _coalesce(scope, self.scope, ''), 72 | _coalesce(icon, self.icon, ''), 73 | _coalesce(flags, self.flags, 0), 74 | ) 75 | 76 | def get(self) -> List[sublime.Region]: 77 | """Return the list of managed regions.""" 78 | return self.view.get_regions(self.key) 79 | 80 | def erase(self) -> None: 81 | """Erase all managed regions. 82 | """ 83 | self.view.erase_regions(self.key) 84 | -------------------------------------------------------------------------------- /st3/sublime_lib/vendor/python/LICENSE: -------------------------------------------------------------------------------- 1 | PSF LICENSE AGREEMENT FOR PYTHON 3.6.5 2 | 3 | 1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and 4 | the Individual or Organization ("Licensee") accessing and otherwise using Python 5 | 3.6.5 software in source or binary form and its associated documentation. 6 | 7 | 2. Subject to the terms and conditions of this License Agreement, PSF hereby 8 | grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, 9 | analyze, test, perform and/or display publicly, prepare derivative works, 10 | distribute, and otherwise use Python 3.6.5 alone or in any derivative 11 | version, provided, however, that PSF's License Agreement and PSF's notice of 12 | copyright, i.e., "Copyright © 2001-2018 Python Software Foundation; All Rights 13 | Reserved" are retained in Python 3.6.5 alone or in any derivative version 14 | prepared by Licensee. 15 | 16 | 3. In the event Licensee prepares a derivative work that is based on or 17 | incorporates Python 3.6.5 or any part thereof, and wants to make the 18 | derivative work available to others as provided herein, then Licensee hereby 19 | agrees to include in any such work a brief summary of the changes made to Python 20 | 3.6.5. 21 | 22 | 4. PSF is making Python 3.6.5 available to Licensee on an "AS IS" basis. 23 | PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF 24 | EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR 25 | WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE 26 | USE OF PYTHON 3.6.5 WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. 27 | 28 | 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON 3.6.5 29 | FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF 30 | MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 3.6.5, OR ANY DERIVATIVE 31 | THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. 32 | 33 | 6. This License Agreement will automatically terminate upon a material breach of 34 | its terms and conditions. 35 | 36 | 7. Nothing in this License Agreement shall be deemed to create any relationship 37 | of agency, partnership, or joint venture between PSF and Licensee. This License 38 | Agreement does not grant permission to use PSF trademarks or trade name in a 39 | trademark sense to endorse or promote products or services of Licensee, or any 40 | third party. 41 | 42 | 8. By copying, installing or otherwise using Python 3.6.5, Licensee agrees 43 | to be bound by the terms and conditions of this License Agreement. 44 | -------------------------------------------------------------------------------- /docs/source/_static/style.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato:400,400i,700,700i|Roboto+Mono:400,400i,700,700i&subset=latin-ext'); 2 | 3 | .related { display: none; } 4 | 5 | *:not(:hover) > a.headerlink { 6 | display: none; 7 | } 8 | 9 | html { 10 | font-size: 20px; 11 | font-family: "Lato","proxima-nova","Helvetica Neue",Arial,sans-serif; 12 | margin: 0; 13 | } 14 | 15 | body { 16 | margin: 0 auto; 17 | 18 | font-size: 14px; 19 | line-height: 1rem; 20 | } 21 | 22 | .document { 23 | display: flex; 24 | justify-content: center; 25 | } 26 | 27 | .sphinxsidebar { 28 | order: 0; 29 | margin-right: 1rem; 30 | } 31 | 32 | .sphinxsidebarwrapper { 33 | position: sticky; 34 | top: 0; 35 | max-height: 100vh; 36 | overflow-y: auto; 37 | padding-right: 20px; 38 | } 39 | 40 | .sphinxsidebarwrapper > ul > li > ul > li { 41 | margin-top: 0.5rem; 42 | } 43 | 44 | .documentwrapper { 45 | order: 1; 46 | max-width: 40rem; 47 | } 48 | 49 | .clearer { display: none; } 50 | 51 | code, pre, 52 | dl.class > dt, dl.function > dt, dl.method > dt, dl.classmethod > dt, dl.attribute > dt 53 | { 54 | font-family: "Roboto Mono",SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",Courier,monospace; 55 | } 56 | 57 | h2 { 58 | margin-top: 1rem; 59 | border-bottom: 2px solid #aaa; 60 | background-color: white; 61 | font-weight: normal; 62 | font-size: 1.5rem; 63 | line-height: 2rem; 64 | } 65 | 66 | #contents > div.container p { 67 | margin-bottom: 0; 68 | } 69 | 70 | #contents > div.container ul { 71 | margin-top: 0; 72 | } 73 | 74 | ul, ol { 75 | padding-left: 1rem; 76 | } 77 | 78 | dd { 79 | margin-left: 1rem; 80 | } 81 | 82 | dd > :first-child { 83 | margin-top: 0.25rem; 84 | } 85 | 86 | dd > ul:only-child, dd > ol:only-child { 87 | padding-left: 0; 88 | } 89 | 90 | code.descname { 91 | font-weight: bold; 92 | font-size: 1.2em; 93 | } 94 | 95 | dl { 96 | margin: 1rem 0; 97 | } 98 | 99 | dl.classmethod > dt > em.property { 100 | display: block; 101 | } 102 | 103 | dl.classmethod > dt > em.property::before { 104 | content: '@'; 105 | display: inline; 106 | } 107 | 108 | li > p:only-child { 109 | margin: 0; 110 | } 111 | 112 | a:not(:hover) { 113 | text-decoration: none; 114 | } 115 | 116 | pre { 117 | border: 1px solid #33333333; 118 | border-radius: 2px; 119 | padding: 0.25rem; 120 | font-size: 13px; 121 | } 122 | 123 | div.footer { 124 | text-align: center; 125 | border-top: 2px solid #aaa; 126 | } 127 | -------------------------------------------------------------------------------- /st3/sublime_lib/encodings.py: -------------------------------------------------------------------------------- 1 | from codecs import lookup 2 | 3 | __all__ = ['from_sublime', 'to_sublime'] 4 | 5 | 6 | def from_sublime(name: str) -> str: 7 | """Translate `name` from a Sublime encoding name to a standard Python encoding name. 8 | 9 | :raise ValueError: if `name` is not a Sublime encoding. 10 | 11 | .. code-block:: python 12 | 13 | >>> from_sublime("Western (Windows 1252)") 14 | "cp1252" 15 | 16 | .. versionchanged:: 1.3 17 | Raise :exc:`ValueError` if `name` is not a Sublime encoding. 18 | """ 19 | 20 | try: 21 | return SUBLIME_TO_STANDARD[name] 22 | except KeyError: 23 | raise ValueError("Unknown Sublime encoding {!r}.".format(name)) from None 24 | 25 | 26 | def to_sublime(name: str) -> str: 27 | """Translate `name` from a standard Python encoding name to a Sublime encoding name. 28 | 29 | :raise ValueError: if `name` is not a Python encoding. 30 | 31 | .. code-block:: python 32 | 33 | >>> to_sublime("cp1252") 34 | "Western (Windows 1252)" 35 | 36 | .. versionchanged:: 1.3 37 | Raise :exc:`ValueError` if `name` is not a Python encoding. 38 | """ 39 | try: 40 | return STANDARD_TO_SUBLIME[lookup(name).name] 41 | except LookupError: 42 | raise ValueError("Unknown Python encoding {!r}.".format(name)) from None 43 | 44 | 45 | SUBLIME_TO_STANDARD = { # noqa: E121 46 | "UTF-8": "utf-8", 47 | "UTF-8 with BOM": "utf-8-sig", 48 | "UTF-16 LE": "utf-16-le", 49 | "UTF-16 LE with BOM": "utf-16", 50 | "UTF-16 BE": "utf-16-be", 51 | "UTF-16 BE with BOM": "utf-16", 52 | "Western (Windows 1252)": "cp1252", 53 | "Western (ISO 8859-1)": "iso8859-1", 54 | "Western (ISO 8859-3)": "iso8859-3", 55 | "Western (ISO 8859-15)": "iso8859-15", 56 | "Western (Mac Roman)": "mac-roman", 57 | "DOS (CP 437)": "cp437", 58 | "Arabic (Windows 1256)": "cp1256", 59 | "Arabic (ISO 8859-6)": "iso8859-6", 60 | "Baltic (Windows 1257)": "cp1257", 61 | "Baltic (ISO 8859-4)": "iso8859-4", 62 | "Celtic (ISO 8859-14)": "iso8859-14", 63 | "Central European (Windows 1250)": "cp1250", 64 | "Central European (ISO 8859-2)": "iso8859-2", 65 | "Central European (Mac)": "mac-latin2", 66 | "Cyrillic (Windows 1251)": "cp1251", 67 | "Cyrillic (Windows 866)": "cp866", 68 | "Cyrillic (ISO 8859-5)": "iso8859-5", 69 | "Cyrillic (KOI8-R)": "koi8-r", 70 | "Cyrillic (KOI8-U)": "koi8-u", 71 | "Estonian (ISO 8859-13)": "iso8859-13", 72 | "Greek (Windows 1253)": "cp1253", 73 | "Greek (ISO 8859-7)": "iso8859-7", 74 | "Hebrew (Windows 1255)": "cp1255", 75 | "Hebrew (ISO 8859-8)": "iso8859-8", 76 | "Nordic (ISO 8859-10)": "iso8859-10", 77 | "Romanian (ISO 8859-16)": "iso8859-16", 78 | "Turkish (Windows 1254)": "cp1254", 79 | "Turkish (ISO 8859-9)": "iso8859-9", 80 | "Vietnamese (Windows 1258)": "cp1258", 81 | } 82 | 83 | 84 | STANDARD_TO_SUBLIME = { # noqa: E121 85 | standard_name: sublime_name 86 | for sublime_name, standard_name in SUBLIME_TO_STANDARD.items() 87 | } 88 | STANDARD_TO_SUBLIME['utf-16'] = 'UTF-16 LE with BOM' 89 | -------------------------------------------------------------------------------- /tests/test_collection_util.py: -------------------------------------------------------------------------------- 1 | from sublime_lib._util.collections import ( 2 | projection, get_selector, isiterable, ismapping, is_sequence_not_str 3 | ) 4 | 5 | from unittest import TestCase 6 | 7 | 8 | class TestSettingsDict(TestCase): 9 | 10 | def test_projection(self): 11 | d = { 12 | 'a': 1, 13 | 'b': 2, 14 | 'c': 3, 15 | } 16 | 17 | self.assertEquals( 18 | projection(d, ()), 19 | {} 20 | ) 21 | 22 | self.assertEquals( 23 | projection(d, ('a', 'c')), 24 | { 25 | 'a': 1, 26 | 'c': 3, 27 | } 28 | ) 29 | 30 | self.assertEquals( 31 | projection(d, { 32 | 'a': 'x', 33 | 'b': 'y', 34 | }), 35 | { 36 | 'x': 1, 37 | 'y': 2, 38 | } 39 | ) 40 | 41 | def test_get_selector(self): 42 | d = { 43 | 'a': 1, 44 | 'b': 2, 45 | 'c': 3, 46 | } 47 | 48 | self.assertEqual( 49 | get_selector('a')(d), 50 | 1 51 | ) 52 | 53 | self.assertEqual( 54 | get_selector('x')(d), 55 | None 56 | ) 57 | 58 | self.assertEqual( 59 | get_selector('x', 42)(d), 60 | 42 61 | ) 62 | 63 | self.assertEqual( 64 | get_selector(lambda d: d['a'])(d), 65 | 1 66 | ) 67 | 68 | self.assertEqual( 69 | get_selector(('a',))(d), 70 | { 71 | 'a': 1, 72 | } 73 | ) 74 | 75 | self.assertEqual( 76 | get_selector(('a', 'b'))(d), 77 | { 78 | 'a': 1, 79 | 'b': 2, 80 | } 81 | ) 82 | 83 | self.assertEqual( 84 | get_selector({'a': 'x', 'b': 'y'})(d), 85 | { 86 | 'x': 1, 87 | 'y': 2, 88 | } 89 | ) 90 | 91 | def test_get_selector_error(self): 92 | with self.assertRaises(TypeError): 93 | get_selector(42) 94 | 95 | def test_isiterable(self): 96 | def generator(): 97 | yield 98 | 99 | self.assertTrue(isiterable('')) 100 | self.assertTrue(isiterable(())) 101 | self.assertTrue(isiterable([])) 102 | self.assertTrue(isiterable(generator())) 103 | 104 | self.assertFalse(isiterable(None)) 105 | self.assertFalse(isiterable(42)) 106 | self.assertFalse(isiterable(object())) 107 | 108 | def test_ismapping(self): 109 | self.assertTrue(ismapping({})) 110 | self.assertFalse(ismapping([])) 111 | 112 | def test_is_sequence_not_str(self): 113 | self.assertTrue(is_sequence_not_str(())) 114 | self.assertTrue(is_sequence_not_str([])) 115 | 116 | self.assertFalse(is_sequence_not_str({})) 117 | self.assertFalse(is_sequence_not_str('')) 118 | -------------------------------------------------------------------------------- /st3/sublime_lib/syntax.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import plistlib 3 | 4 | from ._util.simple_yaml import parse_simple_top_level_keys 5 | from .resource_path import ResourcePath 6 | 7 | from ._compat.typing import List 8 | 9 | __all__ = ['list_syntaxes', 'get_syntax_for_scope'] 10 | 11 | 12 | SyntaxInfo = namedtuple('SyntaxInfo', ['path', 'name', 'scope', 'hidden']) 13 | SyntaxInfo.__new__.__defaults__ = (None, None, False) # type: ignore 14 | 15 | 16 | def get_sublime_syntax_metadata(path: ResourcePath) -> dict: 17 | yaml = parse_simple_top_level_keys(path.read_text()) 18 | return { 19 | 'name': yaml.get('name') or path.stem, 20 | 'hidden': yaml.get('hidden', False), 21 | 'scope': yaml.get('scope'), 22 | } 23 | 24 | 25 | def get_tmlanguage_metadata(path: ResourcePath) -> dict: 26 | tree = plistlib.readPlistFromBytes(path.read_bytes()) 27 | 28 | return { 29 | 'name': tree.get('name') or path.stem, 30 | 'hidden': tree.get('hidden', False), 31 | 'scope': tree.get('scopeName'), 32 | } 33 | 34 | 35 | def get_hidden_tmlanguage_metadata(path: ResourcePath) -> dict: 36 | tree = plistlib.readPlistFromBytes(path.read_bytes()) 37 | 38 | return { 39 | 'name': path.stem, # `name` key is ignored 40 | 'hidden': True, # `hidden` key is ignored 41 | 'scope': tree.get('scopeName'), 42 | } 43 | 44 | 45 | SYNTAX_TYPES = { 46 | '.sublime-syntax': get_sublime_syntax_metadata, 47 | '.tmLanguage': get_tmlanguage_metadata, 48 | '.hidden-tmLanguage': get_hidden_tmlanguage_metadata, 49 | } 50 | 51 | 52 | def get_syntax_metadata(path: ResourcePath) -> SyntaxInfo: 53 | return SyntaxInfo( 54 | path=str(path), 55 | **SYNTAX_TYPES[path.suffix](path) 56 | ) 57 | 58 | 59 | def list_syntaxes() -> List[SyntaxInfo]: 60 | """Return a list of all loaded syntax definitions. 61 | 62 | Each item is a :class:`namedtuple` with the following properties: 63 | 64 | path 65 | The resource path to the syntax definition file. 66 | 67 | name 68 | The display name of the syntax definition. 69 | 70 | scope 71 | The top-level scope of the syntax. 72 | 73 | hidden 74 | Whether the syntax will appear in the syntax menus and the command palette. 75 | """ 76 | syntax_definition_paths = [ 77 | path for path in ResourcePath.glob_resources('') 78 | if path.suffix in SYNTAX_TYPES 79 | ] 80 | 81 | return [ 82 | get_syntax_metadata(path) 83 | for path in syntax_definition_paths 84 | if not ( 85 | path.suffix in {'.tmLanguage', '.hidden-tmLanguage'} 86 | and path.with_suffix('.sublime-syntax') in syntax_definition_paths 87 | ) 88 | ] 89 | 90 | 91 | def get_syntax_for_scope(scope: str) -> str: 92 | """Returns the last syntax in load order that matches `scope`.""" 93 | try: 94 | return next( 95 | syntax.path 96 | for syntax in reversed(list_syntaxes()) 97 | if syntax.scope == scope 98 | ) 99 | except StopIteration: 100 | raise ValueError("Cannot find syntax for scope {!r}.".format(scope)) from None 101 | -------------------------------------------------------------------------------- /tests/test_window_utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from unittest import skipIf 3 | from unittesting import DeferrableTestCase 4 | 5 | from sublime_lib import new_window, close_window 6 | 7 | 8 | class TestNewWindow(DeferrableTestCase): 9 | 10 | def tearDown(self): 11 | # We have to wait between opening and closing a window to avoid a crash. 12 | # See https://github.com/sublimehq/sublime_text/issues/3960 13 | yield 100 14 | if getattr(self, '_window', None): 15 | close_window(self._window, force=True) 16 | 17 | def test_is_valid(self): 18 | self._window = new_window() 19 | self.assertTrue(self._window.is_valid()) 20 | 21 | def test_has_view(self): 22 | self._window = new_window() 23 | self.assertIsNotNone(self._window.active_view()) 24 | 25 | def test_close_window(self): 26 | self._window = new_window() 27 | yield 100 28 | close_window(self._window) 29 | yield 500 30 | self.assertFalse(self._window.is_valid()) 31 | 32 | def test_close_unsaved(self): 33 | self._window = new_window() 34 | 35 | self._window.active_view().run_command('insert', {'characters': 'Hello, World!'}) 36 | 37 | yield 100 38 | with self.assertRaises(ValueError): 39 | close_window(self._window) 40 | 41 | close_window(self._window, force=True) 42 | yield 500 43 | self.assertFalse(self._window.is_valid()) 44 | 45 | def test_menu_visible(self): 46 | self._window = new_window(menu_visible=True) 47 | self.assertTrue(self._window.is_menu_visible()) 48 | 49 | @skipIf(sublime.platform() == 'osx', "Menus are always visible on Mac OS.") 50 | def test_menu_not_visible(self): 51 | self._window = new_window(menu_visible=False) 52 | self.assertFalse(self._window.is_menu_visible()) 53 | 54 | def test_sidebar_visible(self): 55 | self._window = new_window(sidebar_visible=True) 56 | self.assertTrue(self._window.is_sidebar_visible()) 57 | 58 | def test_sidebar_not_visible(self): 59 | self._window = new_window(sidebar_visible=False) 60 | self.assertFalse(self._window.is_sidebar_visible()) 61 | 62 | def test_tabs_visible(self): 63 | self._window = new_window(tabs_visible=True) 64 | self.assertTrue(self._window.get_tabs_visible()) 65 | 66 | def test_tabs_not_visible(self): 67 | self._window = new_window(tabs_visible=False) 68 | self.assertFalse(self._window.get_tabs_visible()) 69 | 70 | def test_minimap_visible(self): 71 | self._window = new_window(minimap_visible=True) 72 | self.assertTrue(self._window.is_minimap_visible()) 73 | 74 | def test_minimap_not_visible(self): 75 | self._window = new_window(minimap_visible=False) 76 | self.assertFalse(self._window.is_minimap_visible()) 77 | 78 | def test_status_bar_visible(self): 79 | self._window = new_window(status_bar_visible=True) 80 | self.assertTrue(self._window.is_status_bar_visible()) 81 | 82 | def test_status_bar_not_visible(self): 83 | self._window = new_window(status_bar_visible=False) 84 | self.assertFalse(self._window.is_status_bar_visible()) 85 | 86 | def test_project_data(self): 87 | data = { 88 | 'folders': [ 89 | {'path': sublime.packages_path()}, 90 | ], 91 | } 92 | 93 | self._window = new_window(project_data=data) 94 | self.assertEqual( 95 | self._window.project_data(), 96 | data 97 | ) 98 | -------------------------------------------------------------------------------- /st3/sublime_lib/window_utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from ._compat.typing import Optional 4 | 5 | from .view_utils import _temporarily_scratch_unsaved_views 6 | 7 | __all__ = ['new_window', 'close_window'] 8 | 9 | 10 | def new_window( 11 | *, 12 | menu_visible: Optional[bool] = None, 13 | sidebar_visible: Optional[bool] = None, 14 | tabs_visible: Optional[bool] = None, 15 | minimap_visible: Optional[bool] = None, 16 | status_bar_visible: Optional[bool] = None, 17 | project_data: Optional[dict] = None 18 | ) -> sublime.Window: 19 | """Open a new window, returning the :class:`~sublime.Window` object. 20 | 21 | This function takes many optional keyword arguments: 22 | 23 | :argument menu_visible: 24 | Show the menubar. 25 | New windows show the menubar by default. 26 | On the Mac OS, this argument has no effect. 27 | 28 | :argument sidebar_visible: 29 | Show the sidebar. 30 | The sidebar will only be shown 31 | if the window's project data has at least one folder. 32 | 33 | :argument tabs_visible: 34 | Show the tab bar. 35 | If the tab bar is hidden, 36 | it will not be shown even if there are multiple tabs. 37 | 38 | :argument minimap_visible: 39 | Show the minimap. 40 | 41 | :argument status_bar_visible: 42 | Show the status bar. 43 | 44 | :argument project_data: 45 | Project data for the window, such as `folders`. 46 | See the `.sublime_project` documentation for details. 47 | 48 | This function currently does not provide a way 49 | to associate a window with a `.sublime_project` file. 50 | 51 | :raise RuntimeError: if the window is not created for any reason. 52 | 53 | .. versionadded:: 1.2 54 | """ 55 | original_ids = set(window.id() for window in sublime.windows()) 56 | 57 | sublime.run_command('new_window') 58 | 59 | try: 60 | window = next(window for window in sublime.windows() if window.id() not in original_ids) 61 | except StopIteration: # pragma: no cover 62 | raise RuntimeError("Window not created.") from None 63 | 64 | if menu_visible is not None: 65 | window.set_menu_visible(menu_visible) 66 | 67 | if sidebar_visible is not None: 68 | window.set_sidebar_visible(sidebar_visible) 69 | 70 | if tabs_visible is not None: 71 | window.set_tabs_visible(tabs_visible) 72 | 73 | if minimap_visible is not None: 74 | window.set_minimap_visible(minimap_visible) 75 | 76 | if status_bar_visible is not None: 77 | window.set_status_bar_visible(status_bar_visible) 78 | 79 | if project_data is not None: 80 | window.set_project_data(project_data) 81 | 82 | return window 83 | 84 | 85 | def close_window(window: sublime.Window, *, force: bool = False) -> None: 86 | """Close the given window, discarding unsaved changes if `force` is ``True``. 87 | 88 | :raise ValueError: if any view in the window has unsaved changes 89 | and `force` is not ``True``. 90 | 91 | .. versionadded:: 1.2 92 | """ 93 | unsaved = [ 94 | view for view in window.views() 95 | if view.is_dirty() and not view.is_scratch() 96 | ] 97 | 98 | if unsaved: 99 | if not force: 100 | raise ValueError('A view has unsaved changes.') 101 | 102 | with _temporarily_scratch_unsaved_views(unsaved): 103 | window.run_command('close_window') 104 | else: 105 | window.run_command('close_window') 106 | -------------------------------------------------------------------------------- /tests/test_output_panel.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import OutputPanel 3 | 4 | from unittest import TestCase 5 | 6 | 7 | class TestOutputPanel(TestCase): 8 | 9 | def setUp(self): 10 | self.window = sublime.active_window() 11 | self.panel_to_restore = self.window.active_panel() 12 | 13 | self.panel_name = "test_panel" 14 | 15 | def tearDown(self): 16 | if getattr(self, 'panel', None): 17 | self.panel.destroy() 18 | 19 | if self.panel_to_restore: 20 | self.window.run_command("show_panel", {"panel": self.panel_to_restore}) 21 | 22 | def assertContents(self, text): 23 | view = self.panel.view 24 | self.assertEqual( 25 | view.substr(sublime.Region(0, view.size())), 26 | text 27 | ) 28 | 29 | def test_stream_operations(self): 30 | self.panel = OutputPanel.create(self.window, self.panel_name) 31 | 32 | self.panel.write("Hello, ") 33 | self.panel.print("World!") 34 | 35 | self.panel.seek_start() 36 | self.panel.print("Top") 37 | 38 | self.panel.seek_end() 39 | self.panel.print("Bottom") 40 | 41 | self.panel.seek(4) 42 | self.panel.print("After Top") 43 | 44 | self.assertContents("Top\nAfter Top\nHello, World!\nBottom\n") 45 | 46 | def test_clear(self): 47 | self.panel = OutputPanel.create(self.window, self.panel_name) 48 | 49 | self.panel.write("Some text") 50 | self.panel.clear() 51 | self.assertContents("") 52 | 53 | def test_show_hide(self): 54 | self.panel = OutputPanel.create(self.window, self.panel_name) 55 | 56 | self.panel.show() 57 | 58 | self.assertTrue(self.panel.is_visible()) 59 | self.assertEqual(self.window.active_panel(), self.panel.full_name) 60 | 61 | self.panel.hide() 62 | 63 | self.assertFalse(self.panel.is_visible()) 64 | self.assertNotEqual(self.window.active_panel(), self.panel.full_name) 65 | 66 | self.panel.toggle_visibility() 67 | 68 | self.assertTrue(self.panel.is_visible()) 69 | self.assertEqual(self.window.active_panel(), self.panel.full_name) 70 | 71 | self.panel.toggle_visibility() 72 | 73 | self.assertFalse(self.panel.is_visible()) 74 | self.assertNotEqual(self.window.active_panel(), self.panel.full_name) 75 | 76 | def test_exists(self): 77 | self.panel = OutputPanel.create(self.window, self.panel_name) 78 | self.assertIsNotNone(self.window.find_output_panel(self.panel.name)) 79 | 80 | def test_destroy(self): 81 | self.panel = OutputPanel.create(self.window, self.panel_name) 82 | self.panel.destroy() 83 | self.assertIsNone(self.window.find_output_panel(self.panel.name)) 84 | 85 | def test_settings(self): 86 | self.panel = OutputPanel.create(self.window, self.panel_name, settings={ 87 | "test_setting": "Hello, World!" 88 | }) 89 | 90 | view_settings = self.panel.view.settings() 91 | self.assertEqual(view_settings.get("test_setting"), "Hello, World!") 92 | 93 | def test_unlisted(self): 94 | self.panel = OutputPanel.create(self.window, self.panel_name, unlisted=True) 95 | 96 | self.panel.show() 97 | self.assertTrue(self.panel.is_visible()) 98 | self.assertNotIn(self.panel.full_name, self.window.panels()) 99 | 100 | def test_attach(self): 101 | self.panel = OutputPanel.create(self.window, self.panel_name, unlisted=True) 102 | 103 | other = OutputPanel(self.window, self.panel_name) 104 | self.assertEqual(self.panel.view.id(), other.view.id()) 105 | 106 | self.panel.destroy() 107 | with self.assertRaises(ValueError): 108 | other.tell() 109 | 110 | def test_init_nonexistent_error(self): 111 | with self.assertRaises(ValueError): 112 | OutputPanel(self.window, 'nonexistent_output_panel') 113 | -------------------------------------------------------------------------------- /tests/test_activity_indicator.py: -------------------------------------------------------------------------------- 1 | from sublime_lib import ActivityIndicator 2 | from sublime_lib.activity_indicator import ViewTarget, WindowTarget 3 | 4 | from sublime import View, Window 5 | 6 | from unittesting import DeferrableTestCase 7 | from unittest.mock import Mock 8 | 9 | 10 | class TestActivityIndicator(DeferrableTestCase): 11 | def test_window_target(self): 12 | message = 'Hello, World!' 13 | window = Mock() 14 | target = WindowTarget(window) 15 | 16 | target.set(message) 17 | window.status_message.assert_called_once_with(message) 18 | 19 | window.status_message.reset_mock() 20 | target.clear() 21 | window.status_message.assert_called_once_with('') 22 | 23 | def test_view_target(self): 24 | key = 'test' 25 | message = 'Hello, World!' 26 | view = Mock() 27 | target = ViewTarget(view, key) 28 | 29 | target.set(message) 30 | view.set_status.assert_called_once_with(key, message) 31 | 32 | target.clear() 33 | view.erase_status.assert_called_once_with(key) 34 | 35 | def test_init_with_view(self): 36 | view = View(0) 37 | indicator = ActivityIndicator(view) 38 | self.assertIsInstance(indicator._target, ViewTarget) 39 | 40 | def test_init_with_window(self): 41 | window = Window(0) 42 | indicator = ActivityIndicator(window) 43 | self.assertIsInstance(indicator._target, WindowTarget) 44 | 45 | def test_tick(self): 46 | target = Mock() 47 | indicator = ActivityIndicator(target) 48 | 49 | results = [ 50 | '[= ]', 51 | '[ = ]', 52 | '[ = ]', 53 | '[ = ]', 54 | '[ = ]', 55 | '[ = ]', 56 | '[ = ]', 57 | '[ = ]', 58 | '[ = ]', 59 | '[ = ]', 60 | '[ =]', 61 | '[ = ]', 62 | '[ = ]', 63 | '[ = ]', 64 | '[ = ]', 65 | '[ = ]', 66 | '[ = ]', 67 | '[ = ]', 68 | '[ = ]', 69 | '[ = ]', 70 | '[= ]', 71 | '[ = ]', 72 | ] 73 | 74 | indicator.update() 75 | for result in results: 76 | target.set.assert_called_once_with(result) 77 | target.set.reset_mock() 78 | indicator.tick() 79 | 80 | def test_label(self): 81 | target = Mock() 82 | indicator = ActivityIndicator(target, 'Hello, World!') 83 | 84 | with indicator: 85 | target.set.assert_called_once_with('Hello, World! [= ]') 86 | 87 | def test_start_stop(self): 88 | target = Mock() 89 | indicator = ActivityIndicator(target) 90 | 91 | indicator.start() 92 | target.set.assert_called_once_with('[= ]') 93 | target.set.reset_mock() 94 | 95 | indicator.stop() 96 | target.clear.assert_called_once_with() 97 | 98 | indicator.start() 99 | target.set.assert_called_once_with('[= ]') 100 | 101 | indicator.stop() 102 | 103 | def test_tick_called(self): 104 | target = Mock() 105 | indicator = ActivityIndicator(target) 106 | with indicator: 107 | target.set.assert_called_once_with('[= ]') 108 | target.set.reset_mock() 109 | yield 150 110 | target.set.assert_called_once_with('[ = ]') 111 | 112 | def test_start_twice_error(self): 113 | target = Mock() 114 | indicator = ActivityIndicator(target) 115 | 116 | with indicator: 117 | with self.assertRaises(ValueError): 118 | indicator.start() 119 | 120 | def test_contextmanager(self): 121 | target = Mock() 122 | indicator = ActivityIndicator(target) 123 | 124 | with indicator: 125 | target.set.assert_called_once_with('[= ]') 126 | 127 | target.clear.assert_called_once_with() 128 | -------------------------------------------------------------------------------- /tests/test_syntax.py: -------------------------------------------------------------------------------- 1 | from sublime_lib import list_syntaxes 2 | from sublime_lib import get_syntax_for_scope 3 | from sublime_lib.syntax import get_syntax_metadata 4 | from sublime_lib.syntax import SyntaxInfo 5 | 6 | from sublime_lib import ResourcePath 7 | 8 | from unittest import TestCase 9 | 10 | 11 | TEST_SYNTAXES_PATH = ResourcePath('Packages/sublime_lib/tests/syntax_test_package') 12 | 13 | 14 | class TestSyntax(TestCase): 15 | 16 | def test_list_syntaxes(self): 17 | syntaxes = list_syntaxes() 18 | self.assertTrue(syntaxes) 19 | 20 | def test_get_syntax(self): 21 | self.assertEqual( 22 | get_syntax_for_scope('source.python'), 23 | 'Packages/Python/Python.sublime-syntax' 24 | ) 25 | 26 | def test_get_syntax_none(self): 27 | 28 | with self.assertRaises(ValueError): 29 | get_syntax_for_scope('sublime_lib.nonexistent_scope') 30 | 31 | 32 | class TestGetMetadata(TestCase): 33 | 34 | def test_defaults(self): 35 | self.assertEqual( 36 | SyntaxInfo(path="a file"), 37 | SyntaxInfo("a file", None, None, False) 38 | ) 39 | 40 | def test_sublime_syntax(self): 41 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test.sublime-syntax' 42 | self.assertEqual( 43 | get_syntax_metadata(path), 44 | SyntaxInfo( 45 | path=str(path), 46 | name="sublime_lib test syntax (sublime-syntax)", 47 | hidden=True, 48 | scope="source.sublime_lib_test", 49 | ) 50 | ) 51 | 52 | def test_sublime_syntax_no_name(self): 53 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_no_name.sublime-syntax' 54 | self.assertEqual( 55 | get_syntax_metadata(path).name, 56 | 'sublime_lib_test_no_name' 57 | ) 58 | 59 | def test_sublime_syntax_null_name(self): 60 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_null_name.sublime-syntax' 61 | self.assertEqual( 62 | get_syntax_metadata(path).name, 63 | 'sublime_lib_test_null_name' 64 | ) 65 | 66 | def test_sublime_syntax_empty_name(self): 67 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_empty_name.sublime-syntax' 68 | self.assertEqual( 69 | get_syntax_metadata(path).name, 70 | 'sublime_lib_test_empty_name' 71 | ) 72 | 73 | def test_tmlanguage_empty_name(self): 74 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_empty_name_tmLanguage.tmLanguage' 75 | self.assertEqual( 76 | get_syntax_metadata(path).name, 77 | 'sublime_lib_test_empty_name_tmLanguage' 78 | ) 79 | 80 | def _syntax_at_path(self, path): 81 | return next(( 82 | info for info in list_syntaxes() if info.path == str(path) 83 | ), None) 84 | 85 | def test_shadowed_tmlanguage(self): 86 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test.tmLanguage' 87 | self.assertTrue(path.exists()) 88 | self.assertIsNone(self._syntax_at_path(path)) 89 | 90 | def test_shadowed_hidden_tmlanguage(self): 91 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test.hidden-tmLanguage' 92 | self.assertTrue(path.exists()) 93 | self.assertIsNone(self._syntax_at_path(path)) 94 | 95 | def test_tmlanguage(self): 96 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_2.tmLanguage' 97 | self.assertEqual( 98 | get_syntax_metadata(path), 99 | SyntaxInfo( 100 | path=str(path), 101 | name="sublime_lib test syntax 2 (tmLanguage)", 102 | hidden=True, 103 | scope="source.sublime_lib_test_2", 104 | ) 105 | ) 106 | 107 | def test_hidden_tmlanguage(self): 108 | path = TEST_SYNTAXES_PATH / 'sublime_lib_test_2.hidden-tmLanguage' 109 | self.assertEqual( 110 | get_syntax_metadata(path), 111 | SyntaxInfo( 112 | path=str(path), 113 | name="sublime_lib_test_2", 114 | hidden=True, 115 | scope="source.sublime_lib_test_2", 116 | ) 117 | ) 118 | -------------------------------------------------------------------------------- /tests/test_region_manager.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime import Region 3 | from sublime_lib import RegionManager, new_view, close_view 4 | 5 | from unittest import TestCase 6 | from unittest.mock import NonCallableMagicMock, MagicMock 7 | 8 | 9 | class ViewMock(NonCallableMagicMock): 10 | def __init__(self): 11 | super().__init__() 12 | 13 | self.add_regions = MagicMock(spec=sublime.View.add_regions) 14 | 15 | 16 | class TestRegionManager(TestCase): 17 | def setUp(self): 18 | self.window = sublime.active_window() 19 | 20 | def tearDown(self): 21 | if getattr(self, 'view', None): 22 | try: 23 | close_view(self.view, force=True) 24 | except ValueError: 25 | pass 26 | 27 | def test_set(self): 28 | self.view = new_view(self.window, scratch=True, content='Hello, World!') 29 | manager = RegionManager(self.view) 30 | 31 | manager.set([Region(0, 5)]) 32 | 33 | self.assertEqual( 34 | manager.get(), 35 | [Region(0, 5)] 36 | ) 37 | 38 | self.assertEqual( 39 | self.view.get_regions(manager.key), 40 | [Region(0, 5)] 41 | ) 42 | 43 | manager.set([Region(7, 13)]) 44 | 45 | self.assertEqual( 46 | manager.get(), 47 | [Region(7, 13)] 48 | ) 49 | 50 | self.assertEqual( 51 | self.view.get_regions(manager.key), 52 | [Region(7, 13)] 53 | ) 54 | 55 | manager.erase() 56 | 57 | self.assertEqual( 58 | manager.get(), 59 | [] 60 | ) 61 | 62 | self.assertEqual( 63 | self.view.get_regions(manager.key), 64 | [] 65 | ) 66 | 67 | def test_del(self): 68 | self.view = new_view(self.window, scratch=True, content='Hello, World!') 69 | manager = RegionManager(self.view) 70 | 71 | manager.set([Region(0, 5)]) 72 | 73 | self.assertEqual( 74 | manager.get(), 75 | [Region(0, 5)] 76 | ) 77 | 78 | self.assertEqual( 79 | self.view.get_regions(manager.key), 80 | [Region(0, 5)] 81 | ) 82 | 83 | key = manager.key 84 | del manager 85 | 86 | self.assertEqual( 87 | self.view.get_regions(key), 88 | [] 89 | ) 90 | 91 | def test_key(self): 92 | self.view = new_view(self.window, scratch=True, content='Hello, World!') 93 | key = 'TestRegionManager' 94 | manager = RegionManager(self.view, key) 95 | 96 | manager.set([Region(0, 5)]) 97 | 98 | self.assertEqual( 99 | manager.get(), 100 | [Region(0, 5)] 101 | ) 102 | 103 | self.assertEqual( 104 | self.view.get_regions(key), 105 | [Region(0, 5)] 106 | ) 107 | 108 | def test_no_default_args(self): 109 | view = ViewMock() 110 | manager = RegionManager(view) 111 | 112 | regions = [Region(0, 5)] 113 | manager.set(regions) 114 | 115 | view.add_regions.assert_called_once_with( 116 | manager.key, 117 | regions, 118 | '', 119 | '', 120 | 0 121 | ) 122 | 123 | def test_args(self): 124 | view = ViewMock() 125 | manager = RegionManager( 126 | view, 127 | scope='region.reddish', 128 | icon='dot', 129 | flags=sublime.DRAW_EMPTY, 130 | ) 131 | 132 | regions = [Region(0, 5)] 133 | manager.set(regions) 134 | 135 | view.add_regions.assert_called_once_with( 136 | manager.key, 137 | regions, 138 | 'region.reddish', 139 | 'dot', 140 | sublime.DRAW_EMPTY 141 | ) 142 | view.add_regions.reset_mock() 143 | 144 | manager.set( 145 | regions, 146 | scope='region.bluish', 147 | icon='circle', 148 | flags=sublime.HIDE_ON_MINIMAP 149 | ) 150 | 151 | view.add_regions.assert_called_once_with( 152 | manager.key, 153 | regions, 154 | 'region.bluish', 155 | 'circle', 156 | sublime.HIDE_ON_MINIMAP 157 | ) 158 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | sys.path.insert(0, os.path.abspath('../../st3')) 18 | sys.path.insert(0, os.path.abspath('extensions')) 19 | sys.path.insert(0, os.path.abspath('mocks')) 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'sublime_lib' 24 | copyright = '2018, Thomas Smith' 25 | author = 'Thomas Smith' 26 | 27 | # The short X.Y version 28 | version = '' 29 | # The full version, including alpha/beta/rc tags 30 | release = '' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinxcontrib.prettyspecialmethods', 46 | ] 47 | 48 | autodoc_member_order = 'bysource' 49 | autodoc_default_options = { 50 | 'members': None, 51 | 'mock_imports': ['sublime_plugin'] 52 | } 53 | 54 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 55 | 56 | # Add any paths that contain templates here, relative to this directory. 57 | templates_path = ['_templates'] 58 | 59 | # The suffix(es) of source filenames. 60 | # You can specify multiple suffix as a list of string: 61 | # 62 | # source_suffix = ['.rst', '.md'] 63 | source_suffix = '.rst' 64 | 65 | # The master toctree document. 66 | master_doc = 'index' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | # This pattern also affects html_static_path and html_extra_path . 78 | exclude_patterns = [] 79 | 80 | # The name of the Pygments (syntax highlighting) style to use. 81 | pygments_style = 'sphinx' 82 | 83 | 84 | # -- Options for HTML output ------------------------------------------------- 85 | 86 | # The theme to use for HTML and HTML Help pages. See the documentation for 87 | # a list of builtin themes. 88 | # 89 | html_theme = 'basic' 90 | 91 | # Theme options are theme-specific and customize the look and feel of a theme 92 | # further. For a list of options available for each theme, see the 93 | # documentation. 94 | # 95 | html_theme_options = { 96 | } 97 | 98 | html_experimental_html5_writer = True 99 | 100 | # Add any paths that contain custom static files (such as style sheets) here, 101 | # relative to this directory. They are copied after the builtin static files, 102 | # so a file named "default.css" will overwrite the builtin "default.css". 103 | html_static_path = ['_static'] 104 | 105 | # Custom sidebar templates, must be a dictionary that maps document names 106 | # to template names. 107 | # 108 | # The default sidebars (for documents that don't match any pattern) are 109 | # defined by theme itself. Builtin themes are using these templates by 110 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 111 | # 'searchbox.html']``. 112 | # 113 | # html_sidebars = {} 114 | 115 | html_sidebars = {'**': [ 116 | "localtoc.html" 117 | ]} 118 | 119 | html_use_index = False 120 | html_use_smartypants = False 121 | html_compact_lists = True 122 | 123 | 124 | def setup(app): 125 | from better_toctree import TocTreeCollector 126 | app.add_env_collector(TocTreeCollector) 127 | 128 | app.add_css_file('style.css') 129 | 130 | from strip_annotations import strip_annotations 131 | app.connect('autodoc-process-signature', strip_annotations) 132 | -------------------------------------------------------------------------------- /st3/sublime_lib/activity_indicator.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from uuid import uuid4 4 | 5 | from ._compat.typing import Optional, Union 6 | from types import TracebackType 7 | from abc import ABCMeta, abstractmethod 8 | from functools import partial 9 | from ._util.weak_method import weak_method 10 | 11 | from threading import Lock 12 | 13 | 14 | __all__ = ['ActivityIndicator'] 15 | 16 | 17 | class StatusTarget(metaclass=ABCMeta): # pragma: no cover 18 | @abstractmethod 19 | def set(self, message: str) -> None: 20 | ... 21 | 22 | @abstractmethod 23 | def clear(self) -> None: 24 | ... 25 | 26 | 27 | class WindowTarget(StatusTarget): 28 | def __init__(self, window: sublime.Window) -> None: 29 | self.window = window 30 | 31 | def set(self, message: str) -> None: 32 | self.window.status_message(message) 33 | 34 | def clear(self) -> None: 35 | self.window.status_message("") 36 | 37 | 38 | class ViewTarget(StatusTarget): 39 | def __init__(self, view: sublime.View, key: Optional[str] = None) -> None: 40 | self.view = view 41 | if key is None: 42 | self.key = '_{!s}'.format(uuid4()) 43 | else: 44 | self.key = key 45 | 46 | def set(self, message: str) -> None: 47 | self.view.set_status(self.key, message) 48 | 49 | def clear(self) -> None: 50 | self.view.erase_status(self.key) 51 | 52 | 53 | class ActivityIndicator: 54 | """ 55 | An animated text-based indicator to show that some activity is in progress. 56 | 57 | The `target` argument should be a :class:`sublime.View` or :class:`sublime.Window`. 58 | The indicator will be shown in the status bar of that view or window. 59 | If `label` is provided, then it will be shown next to the animation. 60 | 61 | :class:`ActivityIndicator` can be used as a context manager. 62 | 63 | .. versionadded:: 1.4 64 | """ 65 | width = 10 # type: int 66 | interval = 100 # type: int 67 | 68 | _target = None # type: StatusTarget 69 | _ticks = 0 # type: int 70 | _lock = None # type: Lock 71 | _running = False # type: bool 72 | _invocation_id = 0 # type: int 73 | 74 | def __init__( 75 | self, 76 | target: Union[StatusTarget, sublime.View, sublime.Window], 77 | label: Optional[str] = None, 78 | ) -> None: 79 | self.label = label 80 | 81 | if isinstance(target, sublime.View): 82 | self._target = ViewTarget(target) 83 | elif isinstance(target, sublime.Window): 84 | self._target = WindowTarget(target) 85 | else: 86 | self._target = target 87 | 88 | self._lock = Lock() 89 | 90 | def __del__(self) -> None: 91 | self._target.clear() 92 | 93 | def __enter__(self) -> None: 94 | self.start() 95 | 96 | def __exit__( 97 | self, 98 | exc_type: type, 99 | exc_value: Exception, 100 | traceback: TracebackType 101 | ) -> None: 102 | self.stop() 103 | 104 | def start(self) -> None: 105 | """ 106 | Start displaying the indicator and animate it. 107 | 108 | :raise ValueError: if the indicator is already running. 109 | """ 110 | with self._lock: 111 | if self._running: 112 | raise ValueError('Timer is already running') 113 | else: 114 | self._running = True 115 | self.update() 116 | sublime.set_timeout( 117 | partial(self._run, self._invocation_id), 118 | self.interval 119 | ) 120 | 121 | def stop(self) -> None: 122 | """ 123 | Stop displaying the indicator. 124 | 125 | If the indicator is not running, do nothing. 126 | """ 127 | with self._lock: 128 | if self._running: 129 | self._running = False 130 | self._invocation_id += 1 131 | self._target.clear() 132 | 133 | def _run(self, invocation_id: int) -> None: 134 | with self._lock: 135 | if invocation_id == self._invocation_id: 136 | self.tick() 137 | sublime.set_timeout( 138 | partial(weak_method(self._run), invocation_id), 139 | self.interval 140 | ) 141 | 142 | def tick(self) -> None: 143 | self._ticks += 1 144 | self.update() 145 | 146 | def update(self) -> None: 147 | self._target.set(self.render(self._ticks)) 148 | 149 | def render(self, ticks: int) -> str: 150 | status = ticks % (2 * self.width) 151 | before = min(status, (2 * self.width) - status) 152 | after = self.width - before 153 | 154 | return "{}[{}={}]".format( 155 | self.label + ' ' if self.label else '', 156 | " " * before, 157 | " " * after, 158 | ) 159 | -------------------------------------------------------------------------------- /st3/sublime_lib/panel.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from .view_stream import ViewStream 4 | from .view_utils import set_view_options, validate_view_options 5 | from ._util.guard import define_guard 6 | 7 | from ._compat.typing import Any 8 | 9 | __all__ = ['Panel', 'OutputPanel'] 10 | 11 | 12 | class Panel(): 13 | """An abstraction of a panel, such as the console or an output panel. 14 | 15 | :raise ValueError: if `window` has no panel called `panel_name`. 16 | 17 | All :class:`Panel` methods except for :meth:`exists()` 18 | will raise a ``ValueError`` if the panel does not exist. 19 | Descendant classes may override :meth:`exists()` to customize this behavior. 20 | 21 | .. py:attribute:: panel_name 22 | 23 | The name of the panel as it is listed in :meth:`sublime.Window.panels()`. 24 | 25 | .. versionadded:: 1.3 26 | """ 27 | 28 | def __init__(self, window: sublime.Window, panel_name: str): 29 | self.window = window 30 | self.panel_name = panel_name 31 | 32 | self._checkExists() 33 | 34 | def _checkExists(self) -> None: 35 | if not self.exists(): 36 | raise ValueError("Panel {} does not exist.".format(self.panel_name)) 37 | 38 | @define_guard 39 | def guard_exists(self) -> None: 40 | self._checkExists() 41 | 42 | def exists(self) -> bool: 43 | """Return ``True`` if the panel exists, or ``False`` otherwise. 44 | 45 | This implementation checks :meth:`sublime.Window.panels()`, 46 | so it will return ``False`` for unlisted panels. 47 | """ 48 | return self.panel_name in self.window.panels() 49 | 50 | @guard_exists 51 | def is_visible(self) -> bool: 52 | """Return ``True`` if the panel is currently visible.""" 53 | return self.window.active_panel() == self.panel_name 54 | 55 | @guard_exists 56 | def show(self) -> None: 57 | """Show the panel, hiding any other visible panel.""" 58 | self.window.run_command("show_panel", {"panel": self.panel_name}) 59 | 60 | @guard_exists 61 | def hide(self) -> None: 62 | """Hide the panel.""" 63 | self.window.run_command("hide_panel", {"panel": self.panel_name}) 64 | 65 | @guard_exists 66 | def toggle_visibility(self) -> None: 67 | """If the panel is visible, hide it; otherwise, show it.""" 68 | if self.is_visible(): 69 | self.hide() 70 | else: 71 | self.show() 72 | 73 | 74 | class OutputPanel(ViewStream, Panel): 75 | """ 76 | A subclass of :class:`~sublime_lib.ViewStream` and :class:`~sublime_lib.Panel` 77 | wrapping an output panel in the given `window` with the given `name`. 78 | 79 | :raise ValueError: if `window` has no output panel called `name`. 80 | 81 | .. versionchanged:: 1.3 82 | Now a subclass of :class:`Panel`. 83 | """ 84 | @classmethod 85 | def create( 86 | cls, 87 | window: sublime.Window, 88 | name: str, 89 | *, 90 | force_writes: bool = False, 91 | follow_cursor: bool = False, 92 | unlisted: bool = False, 93 | **kwargs: Any 94 | ) -> 'OutputPanel': 95 | """Create a new output panel with the given `name` in the given `window`. 96 | 97 | If `kwargs` are given, 98 | they will be interpreted as for :func:`~sublime_lib.view_utils.new_view`. 99 | """ 100 | validate_view_options(kwargs) 101 | 102 | window.destroy_output_panel(name) 103 | view = window.create_output_panel(name, unlisted) 104 | set_view_options(view, **kwargs) 105 | 106 | return cls(window, name, force_writes=force_writes, follow_cursor=follow_cursor) 107 | 108 | def __init__( 109 | self, 110 | window: sublime.Window, 111 | name: str, 112 | *, 113 | force_writes: bool = False, 114 | follow_cursor: bool = False 115 | ): 116 | view = window.find_output_panel(name) 117 | if view is None: 118 | raise ValueError('Output panel "%s" does not exist.' % name) 119 | 120 | ViewStream.__init__(self, view, force_writes=force_writes, follow_cursor=follow_cursor) 121 | Panel.__init__(self, window, "output." + name) 122 | 123 | self.name = name 124 | 125 | @property 126 | def full_name(self) -> str: 127 | """The output panel name, beginning with ``'output.'``. 128 | 129 | Generally, API methods specific to output panels will use :attr:`name`, 130 | while methods that work with any panels will use :attr:`full_name`. 131 | """ 132 | return self.panel_name 133 | 134 | def exists(self) -> bool: 135 | """Return ``True`` if the panel exists, or ``False`` otherwise. 136 | 137 | This implementation checks that the encapsulated :class:`~sublime.View` is valid, 138 | so it will return ``True`` even for unlisted panels. 139 | """ 140 | return self.view.is_valid() 141 | 142 | def destroy(self) -> None: 143 | """Destroy the output panel.""" 144 | self.window.destroy_output_panel(self.name) 145 | -------------------------------------------------------------------------------- /st3/sublime_lib/show_selection_panel.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from ._util.collections import isiterable 4 | from ._util.named_value import NamedValue 5 | from .flags import QuickPanelOption 6 | from collections.abc import Sequence 7 | 8 | from ._compat.typing import Any, Callable, List, Optional, TypeVar, Union, Sequence as _Sequence 9 | 10 | _ItemType = TypeVar('_ItemType') 11 | 12 | __all__ = ['show_selection_panel', 'NO_SELECTION'] 13 | 14 | 15 | NO_SELECTION = NamedValue('NO_SELECTION') 16 | 17 | 18 | def show_selection_panel( 19 | window: sublime.Window, 20 | items: _Sequence[_ItemType], 21 | *, 22 | flags: Any = 0, 23 | labels: Union[_Sequence[object], Callable[[_ItemType], object]] = None, 24 | selected: Union[NamedValue, _ItemType] = NO_SELECTION, 25 | on_select: Optional[Callable[[_ItemType], object]] = None, 26 | on_cancel: Optional[Callable[[], object]] = None, 27 | on_highlight: Optional[Callable[[_ItemType], object]] = None 28 | ) -> None: 29 | """Open a quick panel in the given window to select an item from a list. 30 | 31 | :argument window: The :class:`sublime.Window` in which to show the panel. 32 | 33 | :argument items: A nonempty :class:`~collections.abc.Sequence` 34 | (such as a :class:`list`) of items to choose from. 35 | 36 | Optional keyword arguments: 37 | 38 | :argument flags: A :class:`sublime_lib.flags.QuickPanelOption`, 39 | a value convertible to :class:`~sublime_lib.flags.QuickPanelOption`, 40 | or an iterable of such values. 41 | 42 | :argument labels: A value determining what to show as the label for each item: 43 | 44 | - If `labels` is ``None`` (the default), then use `items`. 45 | - If `labels` is callable, then use ``map(labels, items)``. 46 | - Otherwise, use `labels`. 47 | 48 | The result should be a :class:`~collections.abc.Sequence` of labels. 49 | Every label must be a single item 50 | (a string or convertible with :func:`str`) 51 | or a :class:`~collections.abc.Sequence` of items. 52 | In the latter case, 53 | each entry in the quick panel will show multiple rows. 54 | 55 | :argument selected: The value in `items` that will be initially selected. 56 | 57 | If `selected` is :const:`sublime_lib.NO_SELECTION` (the default), 58 | then Sublime will determine the initial selection. 59 | 60 | :argument on_select: A callback accepting a value from `items` 61 | to be invoked when the user chooses an item. 62 | 63 | :argument on_cancel: A callback that will be invoked with no arguments 64 | if the user closes the panel without choosing an item. 65 | 66 | :argument on_highlight: A callback accepting a value from `items` to be 67 | invoked every time the user changes the highlighted item in the panel. 68 | 69 | :raise ValueError: if `items` is empty. 70 | 71 | :raise ValueError: if `selected` is given and the value is not in `items`. 72 | 73 | :raise ValueError: if `flags` cannot be converted 74 | to :class:`sublime_lib.flags.QuickPanelOption`. 75 | 76 | .. versionadded:: 1.2 77 | 78 | .. versionchanged:: 1.3 79 | `labels` can be a mixture of strings and string sequences of uneven length. 80 | 81 | `flags` can be any value or values 82 | convertible to :class:`~sublime_lib.flags.QuickPanelOption`. 83 | """ 84 | if len(items) == 0: 85 | raise ValueError("The items parameter must contain at least one item.") 86 | 87 | if labels is None: 88 | labels = items 89 | elif callable(labels): 90 | labels = list(map(labels, items)) 91 | elif len(items) != len(labels): 92 | raise ValueError("The lengths of `items` and `labels` must match.") 93 | 94 | def normalize_label(label: object) -> List[str]: 95 | if isinstance(label, Sequence) and not isinstance(label, str): 96 | return list(map(str, label)) 97 | else: 98 | return [str(label)] 99 | 100 | label_strings = list(map(normalize_label, labels)) 101 | max_len = max(map(len, label_strings)) 102 | label_strings = [rows + [''] * (max_len - len(rows)) for rows in label_strings] 103 | 104 | def on_done(index: int) -> None: 105 | if index == -1: 106 | if on_cancel: 107 | on_cancel() 108 | elif on_select: 109 | on_select(items[index]) 110 | 111 | if selected is NO_SELECTION: 112 | selected_index = -1 113 | else: 114 | selected_index = items.index(selected) 115 | 116 | on_highlight_callback = None 117 | if on_highlight: 118 | on_highlight_callback = lambda index: on_highlight(items[index]) 119 | 120 | if isiterable(flags) and not isinstance(flags, str): 121 | flags = QuickPanelOption(*flags) 122 | else: 123 | flags = QuickPanelOption(flags) 124 | 125 | # The signature in the API docs is wrong. 126 | # See https://github.com/SublimeTextIssues/Core/issues/2290 127 | window.show_quick_panel( 128 | items=label_strings, 129 | on_select=on_done, 130 | flags=flags, 131 | selected_index=selected_index, 132 | on_highlight=on_highlight_callback 133 | ) 134 | -------------------------------------------------------------------------------- /tests/test_glob.py: -------------------------------------------------------------------------------- 1 | from sublime_lib._util.glob import get_glob_matcher 2 | 3 | from unittest import TestCase 4 | 5 | 6 | class TestGlob(TestCase): 7 | def _test_matches(self, pattern, positive, negative): 8 | matcher = get_glob_matcher(pattern) 9 | 10 | for path in positive: 11 | if not matcher(path): 12 | raise self.failureException( 13 | "{!r} does not match {!r}".format(pattern, path) 14 | ) 15 | 16 | for path in negative: 17 | if matcher(path): 18 | raise self.failureException( 19 | "{!r} matches {!r}".format(pattern, path) 20 | ) 21 | 22 | def test_recursive_invalid(self): 23 | with self.assertRaises(ValueError): 24 | get_glob_matcher('foo**') 25 | 26 | def test_basic(self): 27 | self._test_matches( 28 | '/Packages/Foo/bar', 29 | [ 30 | 'Packages/Foo/bar', 31 | ], 32 | [ 33 | 'Packages/Foo', 34 | 'Packages/Foo/barr', 35 | 'Packages/Foo/bar/baz', 36 | ] 37 | ) 38 | 39 | self._test_matches( 40 | 'Foo/bar', 41 | [ 42 | 'Foo/bar', 43 | 'Packages/Foo/bar', 44 | ], 45 | [ 46 | 'Packages/Foo/bar/baz', 47 | 'FooFoo/bar', 48 | ] 49 | ) 50 | 51 | def test_star(self): 52 | self._test_matches( 53 | '/Packages/Foo/*', 54 | [ 55 | 'Packages/Foo/bar', 56 | ], 57 | [ 58 | 'Packages/Foo', 59 | 'Packages/Foo/bar/baz', 60 | ] 61 | ) 62 | 63 | self._test_matches( 64 | 'Foo/*', 65 | [ 66 | 'Packages/Foo/bar', 67 | ], 68 | [ 69 | 'Packages/Foo', 70 | 'Packages/Foo/bar/baz', 71 | ] 72 | ) 73 | 74 | self._test_matches( 75 | '/Packages/Foo/A*Z', 76 | [ 77 | 'Packages/Foo/AZ', 78 | 'Packages/Foo/AfoobarZ', 79 | 'Packages/Foo/AAAZZZ', 80 | ], 81 | [ 82 | 'Packages/Foo/AZbar', 83 | 'Packages/Foo/AZ/bar', 84 | 'Packages/Foo/A/Z', 85 | ] 86 | ) 87 | 88 | self._test_matches( 89 | 'Foo/A*Z', 90 | [ 91 | 'Packages/Foo/AZ', 92 | 'Packages/Foo/AfoobarZ', 93 | 'Packages/Foo/AAAZZZ', 94 | ], 95 | [ 96 | 'Packages/Foo/AZbar', 97 | 'Packages/Foo/AZ/bar', 98 | 'Packages/Foo/A/Z', 99 | ] 100 | ) 101 | 102 | def test_recursive(self): 103 | self._test_matches( 104 | '/Packages/Foo/**', 105 | [ 106 | 'Packages/Foo/bar', 107 | 'Packages/Foo/bar/baz', 108 | ], 109 | [ 110 | 'Packages/Foo', 111 | 'Packages/Foobar', 112 | ] 113 | ) 114 | 115 | self._test_matches( 116 | 'Foo/**', 117 | [ 118 | 'Packages/Foo/', 119 | 'Packages/Foo/bar', 120 | 'Packages/Foo/bar/baz', 121 | ], 122 | [ 123 | 'Packages/Foo', 124 | 'Packages/Foobar', 125 | ] 126 | ) 127 | 128 | self._test_matches( 129 | '/Packages/Foo/**/bar', 130 | [ 131 | 'Packages/Foo/bar', 132 | 'Packages/Foo/xyzzy/bar', 133 | ], 134 | [ 135 | 'Packages/Foo/bar/baz', 136 | ] 137 | ) 138 | 139 | self._test_matches( 140 | 'Foo/**/bar', 141 | [ 142 | 'Packages/Foo/bar', 143 | 'Packages/Foo/xyzzy/bar', 144 | ], 145 | [ 146 | 'Packages/Foo/bar/baz', 147 | ] 148 | ) 149 | 150 | self._test_matches( 151 | '/**/Packages/Foo/bar', 152 | [ 153 | 'Packages/Foo/bar', 154 | ], 155 | [] 156 | ) 157 | 158 | self._test_matches( 159 | 'Foo/**/*', 160 | [ 161 | 'Foo/bar', 162 | 'Foo/bar/baz', 163 | ], 164 | [ 165 | 'Foo', 166 | 'Foo/', 167 | ] 168 | ) 169 | 170 | def test_placeholder(self): 171 | self._test_matches( 172 | '/Packages/Foo/ba?', 173 | [ 174 | 'Packages/Foo/bar', 175 | 'Packages/Foo/baz', 176 | ], 177 | [ 178 | 'Packages/Foo/bar/baz', 179 | ] 180 | ) 181 | 182 | def test_range(self): 183 | self._test_matches( 184 | '/Packages/Foo/ba[rz]', 185 | [ 186 | 'Packages/Foo/bar', 187 | 'Packages/Foo/baz', 188 | ], 189 | [ 190 | 'Packages/Foo/bar/baz', 191 | 'Packages/Foo/barr', 192 | 'Packages/Foo/bat', 193 | ] 194 | ) 195 | 196 | self._test_matches( 197 | '/Packages/Foo/ba[a-z]', 198 | [ 199 | 'Packages/Foo/bar', 200 | 'Packages/Foo/baz', 201 | ], 202 | [ 203 | '/Packages/Foo/baR', 204 | ] 205 | ) 206 | -------------------------------------------------------------------------------- /tests/test_view_utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import new_view, close_view, LineEnding 3 | from sublime_lib.view_utils import _clone_view 4 | 5 | from unittest import TestCase 6 | 7 | 8 | class TestViewUtils(TestCase): 9 | 10 | def setUp(self): 11 | self.window = sublime.active_window() 12 | 13 | def tearDown(self): 14 | if getattr(self, 'view', None): 15 | try: 16 | close_view(self.view, force=True) 17 | except ValueError: 18 | pass 19 | 20 | def test_new_view(self): 21 | self.view = new_view(self.window) 22 | 23 | self.assertTrue(self.view.is_valid()) 24 | 25 | self.assertEquals(self.view.name(), '') 26 | self.assertFalse(self.view.is_read_only()) 27 | self.assertFalse(self.view.is_scratch()) 28 | self.assertFalse(self.view.overwrite_status()) 29 | 30 | self.assertEquals(self.view.scope_name(0).strip(), 'text.plain') 31 | 32 | def test_name(self): 33 | self.view = new_view(self.window, name='My Name') 34 | 35 | self.assertEquals(self.view.name(), 'My Name') 36 | 37 | def test_read_only(self): 38 | self.view = new_view(self.window, read_only=True) 39 | 40 | self.assertTrue(self.view.is_read_only()) 41 | 42 | def test_scratch(self): 43 | self.view = new_view(self.window, scratch=True) 44 | 45 | self.assertTrue(self.view.is_scratch()) 46 | 47 | def test_overwrite(self): 48 | self.view = new_view(self.window, overwrite=True) 49 | 50 | self.assertTrue(self.view.overwrite_status()) 51 | 52 | def test_settings(self): 53 | self.view = new_view(self.window, settings={ 54 | 'example_setting': 'Hello, World!', 55 | }) 56 | 57 | self.assertEquals( 58 | self.view.settings().get('example_setting'), 59 | 'Hello, World!' 60 | ) 61 | 62 | def test_scope(self): 63 | self.view = new_view(self.window, scope='source.js') 64 | 65 | self.assertTrue(self.view.scope_name(0).startswith('source.js')) 66 | 67 | def test_syntax(self): 68 | path = 'Packages/JavaScript/JavaScript.sublime-syntax' 69 | self.view = new_view(self.window, syntax=path) 70 | 71 | self.assertEquals( 72 | self.view.settings().get('syntax'), 73 | path 74 | ) 75 | 76 | def test_unknown_args(self): 77 | self.assertRaises( 78 | ValueError, 79 | new_view, 80 | self.window, 81 | bogus_arg="Hello, World!" 82 | ) 83 | 84 | def test_syntax_scope_exclusive(self): 85 | self.assertRaises( 86 | ValueError, 87 | new_view, 88 | self.window, 89 | scope='source.js', 90 | syntax='Packages/JavaScript/JavaScript.sublime-syntax' 91 | ) 92 | 93 | def test_encoding(self): 94 | self.view = new_view(self.window, encoding='utf-16') 95 | 96 | self.assertEquals(self.view.encoding(), "UTF-16 LE with BOM") 97 | 98 | def test_line_endings_unix(self): 99 | self.view = new_view(self.window, line_endings='unix') 100 | 101 | self.assertEquals(self.view.line_endings(), "Unix") 102 | 103 | def test_line_endings_windows(self): 104 | self.view = new_view(self.window, line_endings=LineEnding.Windows) 105 | 106 | self.assertEquals(self.view.line_endings(), "Windows") 107 | 108 | def test_line_endings_cr(self): 109 | self.view = new_view(self.window, line_endings='\r') 110 | 111 | self.assertEquals(self.view.line_endings(), "CR") 112 | 113 | def test_line_endings_invalid(self): 114 | with self.assertRaises(ValueError): 115 | self.view = new_view(self.window, line_endings='other') 116 | 117 | def test_content(self): 118 | self.view = new_view(self.window, content="Hello, World!") 119 | 120 | self.assertEquals( 121 | self.view.substr(sublime.Region(0, self.view.size())), 122 | "Hello, World!" 123 | ) 124 | 125 | def test_content_read_only(self): 126 | self.view = new_view(self.window, content="Hello, World!", read_only=True) 127 | 128 | self.assertEquals( 129 | self.view.substr(sublime.Region(0, self.view.size())), 130 | "Hello, World!" 131 | ) 132 | 133 | def test_close_view(self): 134 | self.view = new_view(self.window) 135 | 136 | close_view(self.view) 137 | self.assertFalse(self.view.is_valid()) 138 | 139 | def test_close_unsaved(self): 140 | self.view = new_view(self.window, content="Hello, World!") 141 | 142 | self.assertRaises(ValueError, close_view, self.view) 143 | self.assertTrue(self.view.is_valid()) 144 | 145 | close_view(self.view, force=True) 146 | self.assertFalse(self.view.is_valid()) 147 | 148 | def test_close_unsaved_clone(self): 149 | self.view = new_view(self.window, content="Hello, World!") 150 | 151 | clone = _clone_view(self.view) 152 | close_view(clone, force=True) 153 | 154 | self.assertFalse(clone.is_valid()) 155 | self.assertTrue(self.view.is_valid()) 156 | self.assertFalse(self.view.is_scratch()) 157 | 158 | def test_close_closed_error(self): 159 | self.view = new_view(self.window) 160 | 161 | close_view(self.view) 162 | self.assertRaises(ValueError, close_view, self.view) 163 | 164 | def test_close_panel_error(self): 165 | view = self.window.create_output_panel('sublime_lib-TestViewUtils') 166 | 167 | self.assertRaises(ValueError, close_view, view) 168 | 169 | self.window.destroy_output_panel('sublime_lib-TestViewUtils') 170 | -------------------------------------------------------------------------------- /stubs/sublime_plugin.pyi: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from typing import Callable, Generic, TypeVar, Optional, Union, List, Tuple, overload 4 | 5 | 6 | InputType = TypeVar('InputType', bound=Union[str, int, float, list, dict, tuple, None]) 7 | 8 | 9 | class CommandInputHandler(Generic[InputType]): 10 | def name(self) -> str: ... 11 | def next_input(self, args: dict) -> Optional[CommandInputHandler]: ... 12 | def placeholder(self) -> str: ... 13 | def initial_text(self) -> str: ... 14 | def preview(self, arg: InputType) -> Union[str, sublime.Html]: ... 15 | def validate(self, arg: InputType) -> bool: ... 16 | def cancel(self) -> None: ... 17 | 18 | @overload 19 | def confirm(self, arg: InputType) -> None: ... 20 | @overload 21 | def confirm(self, arg: InputType, event: dict) -> None: ... 22 | 23 | 24 | class BackInputHandler(CommandInputHandler[None]): 25 | pass 26 | 27 | 28 | class TextInputHandler(CommandInputHandler[str]): 29 | def description(self, text: str) -> str: ... 30 | 31 | 32 | ListItem = Union[str, Tuple[str, InputType]] 33 | 34 | 35 | class ListInputHandler(CommandInputHandler[InputType], Generic[InputType]): 36 | def list_items(self) -> Union[List[ListItem], Tuple[List[ListItem], int]]: ... 37 | def description(self, v: object, text: str) -> str: ... 38 | 39 | 40 | class Command: 41 | def is_enabled(self) -> bool: ... 42 | def is_visible(self) -> bool: ... 43 | def is_checked(self) -> bool: ... 44 | def description(self) -> str: ... 45 | def input(self, args: dict) -> Optional[CommandInputHandler]: ... 46 | def input_description(self) -> str: ... 47 | 48 | 49 | class ApplicationCommand(Command): 50 | run: Callable[..., None] 51 | 52 | 53 | class WindowCommand(Command): 54 | window: sublime.Window 55 | 56 | run: Callable[..., None] 57 | 58 | 59 | class TextCommand(Command): 60 | view: sublime.View 61 | 62 | run: Callable[..., None] 63 | def want_event(self) -> bool: ... 64 | 65 | 66 | Completion = Union[str, Tuple[str, str], List[str]] 67 | 68 | class EventListener: 69 | def on_new(self, view: sublime.View) -> None: ... 70 | def on_new_async(self, view: sublime.View) -> None: ... 71 | def on_clone(self, view: sublime.View) -> None: ... 72 | def on_clone_async(self, view: sublime.View) -> None: ... 73 | def on_load(self, view: sublime.View) -> None: ... 74 | def on_load_async(self, view: sublime.View) -> None: ... 75 | def on_pre_close(self, view: sublime.View) -> None: ... 76 | def on_close(self, view: sublime.View) -> None: ... 77 | def on_pre_save(self, view: sublime.View) -> None: ... 78 | def on_pre_save_async(self, view: sublime.View) -> None: ... 79 | def on_post_save(self, view: sublime.View) -> None: ... 80 | def on_post_save_async(self, view: sublime.View) -> None: ... 81 | def on_modified(self, view: sublime.View) -> None: ... 82 | def on_modified_async(self, view: sublime.View) -> None: ... 83 | def on_selection_modified(self, view: sublime.View) -> None: ... 84 | def on_selection_modified_async(self, view: sublime.View) -> None: ... 85 | def on_activated(self, view: sublime.View) -> None: ... 86 | def on_activated_async(self, view: sublime.View) -> None: ... 87 | def on_deactivated(self, view: sublime.View) -> None: ... 88 | def on_deactivated_async(self, view: sublime.View) -> None: ... 89 | def on_hover(self, view: sublime.View, point: int, hover_zone: int) -> None: ... 90 | def on_query_context(self, view: sublime.View, key: str, operator: int, operand: str, match_all: bool) -> Optional[bool]: ... 91 | def on_query_completions(self, view: sublime.View, prefix: str, locations: List[int]) -> Union[None, List[Completion], Tuple[List[Completion], int]]: ... 92 | def on_text_command(self, view: sublime.View, command_name: str, args: dict) -> Optional[Tuple[str, dict]]: ... 93 | def on_post_text_command(self, view: sublime.View, command_name: str, args: dict) -> None: ... 94 | def on_window_command(self, view: sublime.Window, command_name: str, args: dict) -> Optional[Tuple[str, dict]]: ... 95 | def on_post_window_command(self, view: sublime.Window, command_name: str, args: dict) -> None: ... 96 | 97 | 98 | class ViewEventListener: 99 | view: sublime.View 100 | 101 | @classmethod 102 | def is_applicable(cls, settings: sublime.Settings) -> bool: ... 103 | 104 | @classmethod 105 | def applies_to_primary_view_only(cls) -> bool: ... 106 | 107 | def on_load(self) -> None: ... 108 | def on_load_async(self) -> None: ... 109 | def on_pre_close(self) -> None: ... 110 | def on_close(self) -> None: ... 111 | def on_pre_save(self) -> None: ... 112 | def on_pre_save_async(self) -> None: ... 113 | def on_post_save(self) -> None: ... 114 | def on_post_save_async(self) -> None: ... 115 | def on_modified(self) -> None: ... 116 | def on_modified_async(self) -> None: ... 117 | def on_selection_modified(self) -> None: ... 118 | def on_selection_modified_async(self) -> None: ... 119 | def on_activated_modified(self) -> None: ... 120 | def on_activated_modified_async(self) -> None: ... 121 | def on_deactivated_modified(self) -> None: ... 122 | def on_deactivated_modified_async(self) -> None: ... 123 | def on_hover(self, point: int, hover_zone: int) -> None: ... 124 | def on_query_context(self, key: str, operator: int, operand: str, match_all: bool) -> Optional[bool]: ... 125 | def on_query_completions(self, prefix: str, locations: List[int]) -> Union[None, List[Completion], Tuple[List[Completion], int]]: ... 126 | def on_text_command(self, command_name: str, args: dict) -> Optional[Tuple[str, dict]]: ... 127 | def on_post_text_command(self, command_name: str, args: dict) -> None: ... 128 | -------------------------------------------------------------------------------- /st3/sublime_lib/_compat/typing_stubs.py: -------------------------------------------------------------------------------- 1 | def _MakeType(name): 2 | return _TypeMeta(name, (Type,), {}) 3 | 4 | 5 | class _TypeMeta(type): 6 | def __getitem__(self, args): 7 | if not isinstance(args, tuple): 8 | args = (args,) 9 | 10 | name = '{}[{}]'.format( 11 | str(self), 12 | ', '.join(map(str, args)) 13 | ) 14 | return _MakeType(name) 15 | 16 | def __str__(self): 17 | return self.__name__ 18 | 19 | 20 | __all__ = [ 21 | # Super-special typing primitives. 22 | 'Any', 23 | 'Callable', 24 | 'ClassVar', 25 | 'Generic', 26 | 'Optional', 27 | 'Tuple', 28 | 'Type', 29 | 'TypeVar', 30 | 'Union', 31 | 32 | # ABCs (from collections.abc). 33 | 'AbstractSet', # collections.abc.Set. 34 | 'GenericMeta', # subclass of abc.ABCMeta and a metaclass 35 | # for 'Generic' and ABCs below. 36 | 'ByteString', 37 | 'Container', 38 | 'ContextManager', 39 | 'Hashable', 40 | 'ItemsView', 41 | 'Iterable', 42 | 'Iterator', 43 | 'KeysView', 44 | 'Mapping', 45 | 'MappingView', 46 | 'MutableMapping', 47 | 'MutableSequence', 48 | 'MutableSet', 49 | 'Sequence', 50 | 'Sized', 51 | 'ValuesView', 52 | # The following are added depending on presence 53 | # of their non-generic counterparts in stdlib: 54 | 'Awaitable', 55 | 'AsyncIterator', 56 | 'AsyncIterable', 57 | 'Coroutine', 58 | 'Collection', 59 | 'AsyncGenerator', 60 | # AsyncContextManager 61 | 62 | # Structural checks, a.k.a. protocols. 63 | 'Reversible', 64 | 'SupportsAbs', 65 | 'SupportsBytes', 66 | 'SupportsComplex', 67 | 'SupportsFloat', 68 | 'SupportsInt', 69 | 'SupportsRound', 70 | 71 | # Concrete collection types. 72 | 'Counter', 73 | 'Deque', 74 | 'Dict', 75 | 'DefaultDict', 76 | 'List', 77 | 'Set', 78 | 'FrozenSet', 79 | 'NamedTuple', # Not really a type. 80 | 'Generator', 81 | 82 | # One-off things. 83 | 'AnyStr', 84 | 'cast', 85 | 'get_type_hints', 86 | 'NewType', 87 | 'no_type_check', 88 | 'no_type_check_decorator', 89 | 'overload', 90 | 'Text', 91 | 'TYPE_CHECKING', 92 | 93 | 'ChainMap', 94 | 'NoReturn', 95 | ] 96 | 97 | 98 | def NewType(name, typ): 99 | return _MakeType(name) 100 | 101 | 102 | def TypeVar(name, *types, bound=None): 103 | return _MakeType(name) 104 | 105 | 106 | def cast(typ, val): 107 | return val 108 | 109 | 110 | def get_type_hints(obj, globals=None, locals=None): 111 | return {} 112 | 113 | 114 | def overload(function): 115 | return function 116 | 117 | 118 | def no_type_check(function): 119 | return function 120 | 121 | 122 | def no_type_check_decorator(function): 123 | return function 124 | 125 | 126 | TYPE_CHECKING = False 127 | 128 | 129 | class Type(metaclass=_TypeMeta): 130 | pass 131 | 132 | 133 | class Any(Type): 134 | pass 135 | 136 | 137 | class Callable(Type): 138 | pass 139 | 140 | 141 | class ClassVar(Type): 142 | pass 143 | 144 | 145 | class Generic(Type): 146 | pass 147 | 148 | 149 | class Optional(Type): 150 | pass 151 | 152 | 153 | class Tuple(Type): 154 | pass 155 | 156 | 157 | class Union(Type): 158 | pass 159 | 160 | 161 | class AbstractSet(Type): 162 | pass 163 | 164 | 165 | class GenericMeta(Type): 166 | pass 167 | 168 | 169 | class ByteString(Type): 170 | pass 171 | 172 | 173 | class Container(Type): 174 | pass 175 | 176 | 177 | class ContextManager(Type): 178 | pass 179 | 180 | 181 | class Hashable(Type): 182 | pass 183 | 184 | 185 | class ItemsView(Type): 186 | pass 187 | 188 | 189 | class Iterable(Type): 190 | pass 191 | 192 | 193 | class Iterator(Type): 194 | pass 195 | 196 | 197 | class KeysView(Type): 198 | pass 199 | 200 | 201 | class Mapping(Type): 202 | pass 203 | 204 | 205 | class MappingView(Type): 206 | pass 207 | 208 | 209 | class MutableMapping(Type): 210 | pass 211 | 212 | 213 | class MutableSequence(Type): 214 | pass 215 | 216 | 217 | class MutableSet(Type): 218 | pass 219 | 220 | 221 | class Sequence(Type): 222 | pass 223 | 224 | 225 | class Sized(Type): 226 | pass 227 | 228 | 229 | class ValuesView(Type): 230 | pass 231 | 232 | 233 | class Awaitable(Type): 234 | pass 235 | 236 | 237 | class AsyncIterator(Type): 238 | pass 239 | 240 | 241 | class AsyncIterable(Type): 242 | pass 243 | 244 | 245 | class Coroutine(Type): 246 | pass 247 | 248 | 249 | class Collection(Type): 250 | pass 251 | 252 | 253 | class AsyncGenerator(Type): 254 | pass 255 | 256 | 257 | class AsyncContextManage(Type): 258 | pass 259 | 260 | 261 | class Reversible(Type): 262 | pass 263 | 264 | 265 | class SupportsAbs(Type): 266 | pass 267 | 268 | 269 | class SupportsBytes(Type): 270 | pass 271 | 272 | 273 | class SupportsComplex(Type): 274 | pass 275 | 276 | 277 | class SupportsFloat(Type): 278 | pass 279 | 280 | 281 | class SupportsInt(Type): 282 | pass 283 | 284 | 285 | class SupportsRound(Type): 286 | pass 287 | 288 | 289 | class Counter(Type): 290 | pass 291 | 292 | 293 | class Deque(Type): 294 | pass 295 | 296 | 297 | class Dict(Type): 298 | pass 299 | 300 | 301 | class DefaultDict(Type): 302 | pass 303 | 304 | 305 | class List(Type): 306 | pass 307 | 308 | 309 | class Set(Type): 310 | pass 311 | 312 | 313 | class FrozenSet(Type): 314 | pass 315 | 316 | 317 | class NamedTuple(Type): 318 | pass 319 | 320 | 321 | class Generator(Type): 322 | pass 323 | 324 | 325 | class AnyStr(Type): 326 | pass 327 | 328 | 329 | class Text(Type): 330 | pass 331 | 332 | 333 | class ChainMap(Type): 334 | pass 335 | 336 | 337 | class NoReturn(Type): 338 | pass 339 | -------------------------------------------------------------------------------- /tests/test_settings_dict.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import SettingsDict 3 | 4 | from unittest import TestCase 5 | from collections import ChainMap 6 | 7 | 8 | class TestSettingsDict(TestCase): 9 | 10 | def setUp(self): 11 | self.view = sublime.active_window().new_file() 12 | self.settings = self.view.settings() 13 | self.fancy = SettingsDict(self.settings) 14 | 15 | def tearDown(self): 16 | if self.view: 17 | self.view.set_scratch(True) 18 | self.view.window().focus_view(self.view) 19 | self.view.window().run_command("close_file") 20 | 21 | def test_item(self): 22 | self.settings.set("example_setting", "Hello, World!") 23 | 24 | self.assertEqual(self.fancy['example_setting'], "Hello, World!") 25 | 26 | def test_item_missing_error(self): 27 | self.settings.erase("example_setting") 28 | 29 | self.assertRaises(KeyError, lambda k: self.fancy[k], "example_setting") 30 | 31 | def test_get(self): 32 | self.settings.set("example_setting", "Hello, World!") 33 | 34 | self.assertEqual(self.fancy.get('example_setting'), "Hello, World!") 35 | 36 | def test_get_missing_none(self): 37 | self.settings.erase("example_setting") 38 | 39 | self.assertIsNone(self.fancy.get("example_setting")) 40 | 41 | def test_get_missing_default(self): 42 | self.settings.erase("example_setting") 43 | 44 | self.assertEqual(self.fancy.get("example_setting", "default"), "default") 45 | 46 | def test_set(self): 47 | self.fancy["example_setting"] = "Hello, World!" 48 | 49 | self.assertEqual(self.settings.get('example_setting'), "Hello, World!") 50 | 51 | def test_delete(self): 52 | self.fancy["example_setting"] = "Hello, World!" 53 | del self.fancy["example_setting"] 54 | self.assertNotIn("example_setting", self.fancy) 55 | 56 | def test_delete_missing_error(self): 57 | self.fancy["example_setting"] = "Hello, World!" 58 | del self.fancy["example_setting"] 59 | self.assertRaises(KeyError, self.fancy.__delitem__, "example_setting") 60 | 61 | def test_contains(self): 62 | self.assertNotIn("example_setting", self.fancy) 63 | self.fancy["example_setting"] = "Hello, World!" 64 | self.assertIn("example_setting", self.fancy) 65 | 66 | def test_pop(self): 67 | self.fancy["example_setting"] = "Hello, World!" 68 | result = self.fancy.pop("example_setting") 69 | 70 | self.assertEqual(result, "Hello, World!") 71 | self.assertNotIn("example_setting", self.fancy) 72 | 73 | default = self.fancy.pop("example_setting", 42) 74 | self.assertEqual(default, 42) 75 | 76 | self.assertRaises(KeyError, self.fancy.pop, "example_setting") 77 | 78 | def test_setdefault(self): 79 | result = self.fancy.setdefault("example_setting", "Hello, World!") 80 | 81 | self.assertEqual(result, "Hello, World!") 82 | self.assertEqual(self.fancy["example_setting"], "Hello, World!") 83 | 84 | result = self.fancy.setdefault("example_setting", 42) 85 | 86 | self.assertEqual(result, "Hello, World!") 87 | self.assertEqual(self.fancy["example_setting"], "Hello, World!") 88 | 89 | def test_setdefault_none(self): 90 | result = self.fancy.setdefault("example_setting") 91 | 92 | self.assertEqual(result, None) 93 | self.assertEqual(self.fancy["example_setting"], None) 94 | 95 | def test_update(self): 96 | self.fancy["foo"] = "Hello, World!" 97 | 98 | self.fancy.update({'foo': 1, 'bar': 2}, xyzzy=3) 99 | self.fancy.update([('bar', 20), ('baz', 30)], yzzyx=4) 100 | 101 | self.assertEqual(self.fancy['foo'], 1) 102 | self.assertEqual(self.fancy['bar'], 20) 103 | self.assertEqual(self.fancy['baz'], 30) 104 | self.assertEqual(self.fancy['xyzzy'], 3) 105 | self.assertEqual(self.fancy['yzzyx'], 4) 106 | 107 | def test_not_iterable(self): 108 | self.assertRaises(NotImplementedError, iter, self.fancy) 109 | 110 | def test_get_default(self): 111 | defaults = {'example_1': 'Hello, World!'} 112 | 113 | chained = ChainMap(self.fancy, defaults) 114 | 115 | self.assertNotIn('example_1', self.fancy) 116 | self.assertIn('example_1', chained) 117 | 118 | self.assertEqual(chained['example_1'], 'Hello, World!') 119 | 120 | chained['example_1'] = 'Goodbye, World!' 121 | self.assertEqual(chained['example_1'], 'Goodbye, World!') 122 | self.assertEqual(self.fancy['example_1'], 'Goodbye, World!') 123 | self.assertEqual(defaults['example_1'], 'Hello, World!') 124 | 125 | self.assertRaises(KeyError, chained.__getitem__, 'example_2') 126 | 127 | self.assertRaises(NotImplementedError, iter, chained) 128 | 129 | def test_equal(self): 130 | other = SettingsDict(self.settings) 131 | self.assertEqual(self.fancy, other) 132 | 133 | def test_not_equal(self): 134 | other_view = self.view.window().new_file() 135 | other_view.set_scratch(True) 136 | other = SettingsDict(other_view.settings()) 137 | self.assertNotEqual(self.fancy, other) 138 | 139 | 140 | class TestSettingsDictSubscription(TestCase): 141 | 142 | def setUp(self): 143 | self.view = sublime.active_window().new_file() 144 | self.settings = self.view.settings() 145 | self.fancy = SettingsDict(self.settings) 146 | 147 | def tearDown(self): 148 | if self.view: 149 | self.view.set_scratch(True) 150 | self.view.window().focus_view(self.view) 151 | self.view.window().run_command("close_file") 152 | 153 | def test_subscribe(self): 154 | self.fancy['example_setting'] = 1 155 | 156 | values = None 157 | 158 | def callback(new, old): 159 | nonlocal values 160 | values = (new, old) 161 | 162 | unsubscribe = self.fancy.subscribe('example_setting', callback) 163 | 164 | self.fancy['example_setting'] = 2 165 | self.assertEqual(values, (2, 1)) 166 | 167 | unsubscribe() 168 | self.fancy['example_setting'] = 3 169 | self.assertEqual(values, (2, 1)) 170 | 171 | def test_subscribe_multiple(self): 172 | self.fancy.update( 173 | example_1=1, 174 | example_2=2 175 | ) 176 | 177 | values = None 178 | 179 | def callback(new, old): 180 | nonlocal values 181 | values = new 182 | 183 | self.fancy.subscribe({'example_1', 'example_2', 'example_3'}, callback) 184 | 185 | self.fancy['example_1'] = 10 186 | 187 | self.assertEqual(values, { 188 | 'example_1': 10, 189 | 'example_2': 2 190 | }) 191 | 192 | def test_settings_change_in_callback(self): 193 | calls = [] 194 | 195 | def callback(new, old): 196 | calls.append((new, old)) 197 | self.fancy['bar'] = True 198 | 199 | self.fancy.subscribe('foo', callback) 200 | 201 | self.fancy['foo'] = True 202 | 203 | self.assertEqual(calls, [ 204 | (True, None) 205 | ]) 206 | -------------------------------------------------------------------------------- /st3/sublime_lib/view_utils.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | import inspect 4 | from contextlib import contextmanager 5 | 6 | from ._compat.enum import Enum 7 | from ._util.enum import ExtensibleConstructorMeta, construct_with_alternatives 8 | from .syntax import get_syntax_for_scope 9 | from .encodings import to_sublime 10 | 11 | from ._compat.typing import Any, Optional, Mapping, Iterable, Generator, Type, TypeVar 12 | 13 | 14 | EnumType = TypeVar('EnumType', bound=Enum) 15 | 16 | 17 | __all__ = [ 18 | 'LineEnding', 'new_view', 'close_view', 19 | ] 20 | 21 | 22 | def case_insensitive_value(cls: Type[EnumType], value: str) -> Optional[EnumType]: 23 | return next(( 24 | member for name, member in cls.__members__.items() 25 | if name.lower() == value.lower() 26 | ), None) 27 | 28 | 29 | @construct_with_alternatives(case_insensitive_value) 30 | class LineEnding(Enum, metaclass=ExtensibleConstructorMeta): 31 | """An :class:`Enum` of line endings supported by Sublime Text. 32 | 33 | The :class:`LineEnding` constructor accepts either 34 | the case-insensitive name (e.g. ``'unix'``) or the value (e.g. ``'\\n'``) of a line ending. 35 | 36 | .. py:attribute:: Unix 37 | :annotation: = '\\n' 38 | 39 | .. py:attribute:: Windows 40 | :annotation: = '\\r\\n' 41 | 42 | .. py:attribute:: CR 43 | :annotation: = '\\r' 44 | 45 | .. versionadded:: 1.2 46 | """ 47 | Unix = '\n' 48 | Windows = '\r\n' 49 | CR = '\r' 50 | 51 | 52 | def new_view(window: sublime.Window, **kwargs: Any) -> sublime.View: 53 | """Open a new view in the given `window`, returning the :class:`~sublime.View` object. 54 | 55 | This function takes many optional keyword arguments: 56 | 57 | :argument content: Text to be inserted into the new view. The text will be inserted even 58 | if the `read_only` option is ``True``. 59 | 60 | :argument encoding: The encoding that the view should use when saving. 61 | 62 | :argument line_endings: The kind of line endings to use. 63 | The given value will be passed to :class:`LineEnding`. 64 | 65 | :argument name: The name of the view. This will be shown as the title of the view's tab. 66 | 67 | :argument overwrite: If ``True``, the view will be in overwrite mode. 68 | 69 | :argument read_only: If ``True``, the view will be read-only. 70 | 71 | :argument scope: A scope name. 72 | The view will be assigned a syntax definition that corresponds to the given scope 73 | (as determined by :func:`~sublime_lib.get_syntax_for_scope`). 74 | Incompatible with the `syntax` option. 75 | 76 | :argument scratch: If ``True``, the view will be a scratch buffer. 77 | The user will not be prompted to save the view before closing it. 78 | 79 | :argument settings: A dictionary of names and values 80 | that will be applied to the new view's Settings object. 81 | 82 | :argument syntax: The resource path of a syntax definition that the view will use. 83 | Incompatible with the `scope` option. 84 | 85 | :raise ValueError: if both `scope` and `syntax` are specified. 86 | :raise ValueError: if `encoding` is not a Python encoding name. 87 | :raise ValueError: if `line_endings` cannot be converted to :class:`LineEnding`. 88 | 89 | .. versionchanged:: 1.2 90 | Added the `line_endings` argument. 91 | """ 92 | validate_view_options(kwargs) 93 | 94 | view = window.new_file() 95 | set_view_options(view, **kwargs) 96 | return view 97 | 98 | 99 | @contextmanager 100 | def _temporarily_scratch_unsaved_views( 101 | unsaved_views: Iterable[sublime.View] 102 | ) -> Generator[None, None, None]: 103 | buffer_ids = {view.buffer_id() for view in unsaved_views} 104 | for view in unsaved_views: 105 | view.set_scratch(True) 106 | 107 | try: 108 | yield 109 | finally: 110 | clones = { 111 | view.buffer_id(): view 112 | for window in sublime.windows() 113 | for view in window.views() 114 | if view.buffer_id() in buffer_ids 115 | } 116 | for view in clones.values(): 117 | view.set_scratch(False) 118 | 119 | 120 | def _clone_view(view: sublime.View) -> sublime.View: 121 | window = view.window() 122 | if window is None: # pragma: no cover 123 | raise ValueError("View has no window.") 124 | 125 | window.focus_view(view) 126 | window.run_command('clone_file') 127 | clone = window.active_view() 128 | if clone is None: # pragma: no cover 129 | raise RuntimeError("Clone was not created.") 130 | 131 | return clone 132 | 133 | 134 | def close_view(view: sublime.View, *, force: bool = False) -> None: 135 | """Close the given view, discarding unsaved changes if `force` is ``True``. 136 | 137 | If the view is invalid (e.g. already closed), do nothing. 138 | 139 | :raise ValueError: if the view has unsaved changes and `force` is not ``True``. 140 | :raise ValueError: if the view is not closed for any other reason. 141 | """ 142 | unsaved = view.is_dirty() and not view.is_scratch() 143 | 144 | if unsaved: 145 | if not force: 146 | raise ValueError('The view has unsaved changes.') 147 | 148 | with _temporarily_scratch_unsaved_views([view]): 149 | closed = view.close() 150 | else: 151 | closed = view.close() 152 | 153 | if not closed: 154 | raise ValueError('The view could not be closed.') 155 | 156 | 157 | def validate_view_options(options: Mapping[str, Any]) -> None: 158 | unknown = set(options) - VIEW_OPTIONS 159 | if unknown: 160 | raise ValueError('Unknown view options: %s.' % ', '.join(list(unknown))) 161 | 162 | if 'scope' in options and 'syntax' in options: 163 | raise ValueError('The "syntax" and "scope" arguments are exclusive.') 164 | 165 | if 'line_endings' in options: 166 | LineEnding(options['line_endings']) 167 | 168 | 169 | def set_view_options( 170 | view: sublime.View, 171 | *, 172 | name: Optional[str] = None, 173 | settings: Optional[dict] = None, 174 | read_only: Optional[bool] = None, 175 | scratch: Optional[bool] = None, 176 | overwrite: Optional[bool] = None, 177 | syntax: Optional[str] = None, 178 | scope: Optional[str] = None, 179 | encoding: Optional[str] = None, 180 | content: Optional[str] = None, 181 | line_endings: Optional[str] = None 182 | ) -> None: 183 | if name is not None: 184 | view.set_name(name) 185 | 186 | if content is not None: 187 | view.run_command('append', {'characters': content}) 188 | 189 | if settings is not None: 190 | view_settings = view.settings() 191 | for key, value in settings.items(): 192 | view_settings.set(key, value) 193 | 194 | if read_only is not None: 195 | view.set_read_only(read_only) 196 | 197 | if scratch is not None: 198 | view.set_scratch(scratch) 199 | 200 | if overwrite is not None: 201 | view.set_overwrite_status(overwrite) 202 | 203 | if scope is not None: 204 | view.assign_syntax(get_syntax_for_scope(scope)) 205 | 206 | if syntax is not None: 207 | view.assign_syntax(syntax) 208 | 209 | if encoding is not None: 210 | view.set_encoding(to_sublime(encoding)) 211 | 212 | if line_endings is not None: 213 | view.set_line_endings(LineEnding(line_endings).name) 214 | 215 | 216 | VIEW_OPTIONS = { 217 | name 218 | for name, param in inspect.signature(set_view_options).parameters.items() 219 | if param.kind == inspect.Parameter.KEYWORD_ONLY 220 | } 221 | -------------------------------------------------------------------------------- /st3/sublime_lib/view_stream.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime import Region 3 | 4 | from contextlib import contextmanager 5 | from io import SEEK_SET, SEEK_CUR, SEEK_END, TextIOBase 6 | 7 | from ._util.guard import define_guard 8 | 9 | from ._compat.typing import Any, Generator 10 | 11 | 12 | class ViewStream(TextIOBase): 13 | """A :class:`~io.TextIOBase` encapsulating a :class:`~sublime.View` object. 14 | 15 | All public methods (except :meth:`flush`) require 16 | that the underlying View object be valid (using :meth:`View.is_valid`). 17 | Otherwise, :class:`ValueError` will be raised. 18 | 19 | The :meth:`read`, :meth:`readline`, :meth:`write`, :meth:`print`, 20 | and :meth:`tell` methods 21 | require that the underlying View have exactly one selection, 22 | and that the selection is empty (i.e. a simple cursor). 23 | Otherwise, :class:`ValueError` will be raised. 24 | 25 | :argument force_writes: If ``True``, then :meth:`write` and :meth:`print` 26 | will write to the view even if it is read-only. 27 | Otherwise, those methods will raise :exc:`ValueError`. 28 | 29 | :argument follow_cursor: If ``True``, then any method 30 | that moves the cursor position will scroll the view 31 | to ensure that the new position is visible. 32 | 33 | .. versionchanged:: 1.2 34 | Added the `follow_cursor` option. 35 | """ 36 | 37 | @define_guard 38 | @contextmanager 39 | def guard_read_only(self) -> Generator[Any, None, None]: 40 | if self.view.is_read_only(): 41 | if self.force_writes: 42 | self.view.set_read_only(False) 43 | yield 44 | self.view.set_read_only(True) 45 | else: 46 | raise ValueError("The underlying view is read-only.") 47 | else: 48 | yield 49 | 50 | @define_guard 51 | @contextmanager 52 | def guard_auto_indent(self) -> Generator[Any, None, None]: 53 | settings = self.view.settings() 54 | if settings.get('auto_indent'): 55 | settings.set('auto_indent', False) 56 | yield 57 | settings.set('auto_indent', True) 58 | else: 59 | yield 60 | 61 | @define_guard 62 | def guard_validity(self) -> None: 63 | if not self.view.is_valid(): 64 | raise ValueError("The underlying view is invalid.") 65 | 66 | @define_guard 67 | def guard_selection(self) -> None: 68 | if len(self.view.sel()) == 0: 69 | raise ValueError("The underlying view has no selection.") 70 | elif len(self.view.sel()) > 1: 71 | raise ValueError("The underlying view has multiple selections.") 72 | elif not self.view.sel()[0].empty(): 73 | raise ValueError("The underlying view's selection is not empty.") 74 | 75 | def __init__( 76 | self, view: sublime.View, *, force_writes: bool = False, follow_cursor: bool = False 77 | ): 78 | self.view = view 79 | self.force_writes = force_writes 80 | self.follow_cursor = follow_cursor 81 | 82 | @guard_validity 83 | @guard_selection 84 | def read(self, size: int = -1) -> str: 85 | """Read and return at most `size` characters from the stream as a single :class:`str`. 86 | 87 | If `size` is negative or None, read until EOF. 88 | """ 89 | begin = self._tell() 90 | end = self.view.size() 91 | 92 | if size is None: 93 | size = -1 94 | 95 | return self._read(begin, end, size) 96 | 97 | @guard_validity 98 | @guard_selection 99 | def readline(self, size: int = -1) -> str: 100 | """Read and return one line from the stream, to a maximum of `size` characters. 101 | 102 | If the stream is already at EOF, return an empty string. 103 | """ 104 | begin = self._tell() 105 | end = self.view.full_line(begin).end() 106 | 107 | return self._read(begin, end, size) 108 | 109 | def _read(self, begin: int, end: int, size: int) -> str: 110 | if size >= 0: 111 | end = min(end, begin + size) 112 | 113 | self._seek(end) 114 | return self.view.substr(Region(begin, end)) 115 | 116 | @guard_validity 117 | @guard_selection 118 | @guard_read_only 119 | @guard_auto_indent 120 | def write(self, s: str) -> int: 121 | """Insert the string `s` into the view immediately before the cursor 122 | and return the number of characters inserted. 123 | 124 | Because Sublime may convert tabs to spaces, 125 | the number of characters inserted may not match 126 | the length of the argument. 127 | """ 128 | old_size = self.view.size() 129 | self.view.run_command('insert', {'characters': s}) 130 | self._maybe_show_cursor() 131 | return self.view.size() - old_size 132 | 133 | def print(self, *objects: object, sep: str = ' ', end: str = '\n') -> None: 134 | """Shorthand for :func:`print()` passing this ViewStream as the `file` argument.""" 135 | print(*objects, file=self, sep=sep, end=end) # type: ignore 136 | 137 | def flush(self) -> None: 138 | """Do nothing. (The stream is not buffered.)""" 139 | pass 140 | 141 | @guard_validity 142 | def seek(self, offset: int, whence: int = SEEK_SET) -> int: 143 | """Move the cursor in the view and return the new offset. 144 | 145 | If `whence` is provided, 146 | the behavior is the same as for :class:`~io.IOBase`. 147 | If the cursor would move before the beginning of the view, 148 | it will move to the beginning instead, 149 | and likewise for the end of the view. 150 | If the view had multiple selections, none will be preserved. 151 | 152 | .. versionchanged:: 1.2 153 | Allow non-zero arguments with any value of `whence`. 154 | """ 155 | if whence == SEEK_SET: 156 | return self._seek(offset) 157 | elif whence == SEEK_CUR: 158 | return self._seek(self._tell() + offset) 159 | elif whence == SEEK_END: 160 | return self._seek(self.view.size() + offset) 161 | else: 162 | raise TypeError('Invalid value for argument "whence".') 163 | 164 | def _seek(self, offset: int) -> int: 165 | selection = self.view.sel() 166 | selection.clear() 167 | selection.add(Region(offset)) 168 | self._maybe_show_cursor() 169 | return self._tell() 170 | 171 | @guard_validity 172 | def seek_start(self) -> None: 173 | """Move the cursor in the view to before the first character.""" 174 | self._seek(0) 175 | 176 | @guard_validity 177 | def seek_end(self) -> None: 178 | """Move the cursor in the view to after the last character.""" 179 | self._seek(self.view.size()) 180 | 181 | @guard_validity 182 | @guard_selection 183 | def tell(self) -> int: 184 | """Return the character offset of the cursor.""" 185 | return self._tell() 186 | 187 | def _tell(self) -> int: 188 | return self.view.sel()[0].b 189 | 190 | @guard_validity 191 | @guard_selection 192 | def show_cursor(self) -> None: 193 | """Scroll the view to show the position of the cursor.""" 194 | self._show_cursor() 195 | 196 | def _show_cursor(self) -> None: 197 | self.view.show(self._tell()) 198 | 199 | def _maybe_show_cursor(self) -> None: 200 | if self.follow_cursor: 201 | self._show_cursor() 202 | 203 | @guard_validity 204 | @guard_selection 205 | @guard_read_only 206 | def clear(self) -> None: 207 | """Erase all text in the view.""" 208 | self.view.run_command('select_all') 209 | self.view.run_command('left_delete') 210 | -------------------------------------------------------------------------------- /st3/sublime_lib/settings_dict.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | 3 | from uuid import uuid4 4 | from functools import partial 5 | from collections.abc import Mapping 6 | 7 | from ._util.collections import get_selector 8 | from ._util.named_value import NamedValue 9 | 10 | from ._compat.typing import Any, Callable, Iterable, NoReturn, TypeVar, Union, Mapping as _Mapping 11 | 12 | _Default = TypeVar('_Default') 13 | Value = Union[bool, int, float, str, list, dict, None] 14 | 15 | __all__ = ['SettingsDict', 'NamedSettingsDict'] 16 | 17 | 18 | _NO_DEFAULT = NamedValue('SettingsDict.NO_DEFAULT') 19 | 20 | 21 | class SettingsDict(): 22 | """Wraps a :class:`sublime.Settings` object `settings` 23 | with a :class:`dict`-like interface. 24 | 25 | There is no way to list or iterate over the keys of a 26 | :class:`~sublime.Settings` object. As a result, the following methods are 27 | not implemented: 28 | 29 | - :meth:`__len__` 30 | - :meth:`__iter__` 31 | - :meth:`clear` 32 | - :meth:`copy` 33 | - :meth:`items` 34 | - :meth:`keys` 35 | - :meth:`popitem` 36 | - :meth:`values` 37 | 38 | You can use :class:`collections.ChainMap` to chain a :class:`SettingsDict` 39 | with other dict-like objects. If you do, calling the above unimplemented 40 | methods on the :class:`~collections.ChainMap` will raise an error. 41 | """ 42 | 43 | NO_DEFAULT = _NO_DEFAULT 44 | 45 | def __init__(self, settings: sublime.Settings): 46 | self.settings = settings 47 | 48 | def __iter__(self) -> NoReturn: 49 | """Raise NotImplementedError.""" 50 | raise NotImplementedError() 51 | 52 | def __eq__(self, other: object) -> bool: 53 | """Return ``True`` if `self` and `other` are of the same type 54 | and refer to the same underlying settings data. 55 | """ 56 | return ( 57 | type(self) == type(other) 58 | and isinstance(other, SettingsDict) 59 | and self.settings.settings_id == other.settings.settings_id 60 | ) 61 | 62 | def __getitem__(self, key: str) -> Value: 63 | """Return the setting named `key`. 64 | 65 | If a subclass of :class:`SettingsDict` defines a method :meth:`__missing__` 66 | and `key` is not present, 67 | the `d[key]` operation calls that method with `key` as the argument. 68 | The `d[key]` operation then returns or raises 69 | whatever is returned or raised by the ``__missing__(key)`` call. 70 | No other operations or methods invoke :meth:`__missing__`. 71 | If :meth:`__missing__` is not defined, :exc:`KeyError` is raised. 72 | :meth:`__missing__` must be a method; it cannot be an instance variable. 73 | 74 | :raise KeyError: if there is no setting with the given `key` 75 | and :meth:`__missing__` is not defined. 76 | """ 77 | if key in self: 78 | return self.get(key) 79 | else: 80 | return self.__missing__(key) 81 | 82 | def __missing__(self, key: str) -> Value: 83 | raise KeyError(key) 84 | 85 | def __setitem__(self, key: str, value: Value) -> None: 86 | """Set `self[key]` to `value`.""" 87 | self.settings.set(key, value) 88 | 89 | def __delitem__(self, key: str) -> None: 90 | """Remove `self[key]` from `self`. 91 | 92 | :raise KeyError: if there us no setting with the given `key`. 93 | """ 94 | if key in self: 95 | self.settings.erase(key) 96 | else: 97 | raise KeyError(key) 98 | 99 | def __contains__(self, item: str) -> bool: 100 | """Return ``True`` if `self` has a setting named `key`, else ``False``.""" 101 | return self.settings.has(item) 102 | 103 | def get(self, key: str, default: _Default = None) -> Union[Value, _Default]: 104 | """Return the value for `key` if `key` is in the dictionary, or `default` otherwise. 105 | 106 | If `default` is not given, it defaults to ``None``, 107 | so that this method never raises :exc:`KeyError`.""" 108 | return self.settings.get(key, default) 109 | 110 | def pop( 111 | self, key: str, default: Union[_Default, NamedValue] = _NO_DEFAULT 112 | ) -> Union[Value, _Default]: 113 | """Remove the setting `self[key]` and return its value or `default`. 114 | 115 | :raise KeyError: if `key` is not in the dictionary 116 | and `default` is :attr:`SettingsDict.NO_DEFAULT`. 117 | 118 | .. versionchanged:: 1.2 119 | Added :attr:`SettingsDict.NO_DEFAULT`. 120 | """ 121 | if key in self: 122 | ret = self[key] 123 | del self[key] 124 | return ret 125 | elif default is _NO_DEFAULT: 126 | raise KeyError(key) 127 | else: 128 | return default # type: ignore 129 | 130 | def setdefault(self, key: str, default: Value = None) -> Value: 131 | """Set `self[key]` to `default` if it wasn't already defined and return `self[key]`. 132 | """ 133 | if key in self: 134 | return self[key] 135 | else: 136 | self[key] = default 137 | return default 138 | 139 | def update( 140 | self, 141 | other: Union[_Mapping[str, Value], Iterable[Iterable[str]]] = [], 142 | **kwargs: Value 143 | ) -> None: 144 | """Update the dictionary with the key/value pairs from `other`, 145 | overwriting existing keys. 146 | 147 | Accepts either another dictionary object 148 | or an iterable of key/value pairs (as tuples or other iterables of length two). 149 | If keyword arguments are specified, 150 | the dictionary is then updated with those key/value pairs: 151 | ``self.update(red=1, blue=2)``. 152 | """ 153 | if isinstance(other, Mapping): 154 | other = other.items() # type: ignore 155 | 156 | for key, value in other: 157 | self[key] = value 158 | 159 | for key, value in kwargs.items(): 160 | self[key] = value 161 | 162 | def subscribe( 163 | self, selector: Any, callback: Callable, default_value: Any = None 164 | ) -> Callable[[], None]: 165 | """Register a callback to be invoked 166 | when the value derived from the settings object changes 167 | and return a function that when invoked will unregister the callback. 168 | 169 | Instead of passing the `SettingsDict` to callback, 170 | a value derived using `selector` is passed. 171 | If `selector` is callable, then ``selector(self)`` is passed. 172 | If `selector` is a :class:`str`, 173 | then ``self.get(selector, default_value)`` is passed. 174 | Otherwise, ``projection(self, selector)`` is passed. 175 | 176 | Changes in the selected value are detected 177 | by comparing the last known value to the current value 178 | using the equality operator. 179 | If you use a selector function, 180 | the result must be equatable and should not be mutated. 181 | 182 | `callback` should accept two arguments: 183 | the new derived value and the previous derived value. 184 | 185 | .. versionchanged:: 1.1 186 | Return an unsubscribe callback. 187 | """ 188 | selector_fn = get_selector(selector) 189 | 190 | saved_value = selector_fn(self) 191 | 192 | def onchange() -> None: 193 | nonlocal saved_value 194 | new_value = selector_fn(self) 195 | 196 | if new_value != saved_value: 197 | previous_value = saved_value 198 | saved_value = new_value 199 | callback(new_value, previous_value) 200 | 201 | key = str(uuid4()) 202 | self.settings.add_on_change(key, onchange) 203 | return partial(self.settings.clear_on_change, key) 204 | 205 | 206 | class NamedSettingsDict(SettingsDict): 207 | """Wraps a :class:`sublime.Settings` object corresponding to a `sublime-settings` file.""" 208 | 209 | @property 210 | def file_name(self) -> str: 211 | """The name of the sublime-settings files 212 | associated with the :class:`NamedSettingsDict`.""" 213 | return self.name + '.sublime-settings' 214 | 215 | def __init__(self, name: str): 216 | """Return a new :class:`NamedSettingsDict` corresponding to the given name.""" 217 | 218 | if name.endswith('.sublime-settings'): 219 | self.name = name[:-17] 220 | else: 221 | self.name = name 222 | 223 | super().__init__(sublime.load_settings(self.file_name)) 224 | 225 | def save(self) -> None: 226 | """Flush any in-memory changes to the :class:`NamedSettingsDict` to disk.""" 227 | sublime.save_settings(self.file_name) 228 | -------------------------------------------------------------------------------- /tests/test_view_stream.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import ViewStream 3 | 4 | from unittesting import DeferrableTestCase 5 | from io import UnsupportedOperation, StringIO 6 | 7 | 8 | class TestViewStream(DeferrableTestCase): 9 | 10 | def setUp(self): 11 | self.view = sublime.active_window().new_file() 12 | self.stream = ViewStream(self.view) 13 | 14 | def tearDown(self): 15 | if self.view: 16 | self.view.set_scratch(True) 17 | self.view.close() 18 | 19 | def assertContents(self, text): 20 | self.assertEqual( 21 | self.view.substr(sublime.Region(0, self.view.size())), 22 | text 23 | ) 24 | 25 | def test_stream_operations(self): 26 | self.stream.write("Hello, ") 27 | self.stream.print("World!") 28 | self.assertEqual(self.stream.tell(), 14) 29 | 30 | self.stream.seek_start() 31 | self.assertEqual(self.stream.tell(), 0) 32 | self.stream.print("Top") 33 | 34 | self.stream.seek_end() 35 | self.assertEqual(self.stream.tell(), 18) 36 | self.stream.print("Bottom") 37 | 38 | self.stream.seek(4) 39 | self.stream.print("After Top") 40 | 41 | self.assertContents("Top\nAfter Top\nHello, World!\nBottom\n") 42 | 43 | def test_write_size(self): 44 | text = "Hello\n\tWorld!" 45 | 46 | size = self.stream.write(text) 47 | self.assertEqual(size, self.stream.view.size()) 48 | 49 | def test_no_indent(self): 50 | text = " " 51 | 52 | self.stream.view.settings().set('auto_indent', True) 53 | self.stream.write(text) 54 | self.stream.write("\n") 55 | self.assertContents(text + "\n") 56 | 57 | self.assertTrue(self.stream.view.settings().get('auto_indent')) 58 | 59 | def test_no_indent_off(self): 60 | text = " " 61 | 62 | self.stream.view.settings().set('auto_indent', False) 63 | self.stream.write(text) 64 | self.stream.write("\n") 65 | self.assertContents(text + "\n") 66 | 67 | self.assertFalse(self.stream.view.settings().get('auto_indent')) 68 | 69 | def test_clear(self): 70 | self.stream.write("Some text") 71 | self.stream.clear() 72 | self.assertContents("") 73 | 74 | def test_read(self): 75 | self.stream.write("Hello, World!\nGoodbye, World!") 76 | 77 | self.stream.seek(7) 78 | text = self.stream.read(5) 79 | self.assertEqual(text, "World") 80 | self.assertEqual(self.stream.tell(), 12) 81 | 82 | text = self.stream.read(3) 83 | self.assertEqual(text, "!\nG") 84 | self.assertEqual(self.stream.tell(), 15) 85 | 86 | text = self.stream.read(1000) 87 | self.assertEqual(text, "oodbye, World!") 88 | self.assertEqual(self.stream.tell(), self.stream.view.size()) 89 | 90 | self.stream.seek(7) 91 | text = self.stream.read(None) 92 | self.assertEqual(text, "World!\nGoodbye, World!") 93 | 94 | self.stream.seek(7) 95 | text = self.stream.read(-1) 96 | self.assertEqual(text, "World!\nGoodbye, World!") 97 | 98 | self.stream.seek(7) 99 | text = self.stream.read() 100 | self.assertEqual(text, "World!\nGoodbye, World!") 101 | 102 | def test_readline(self): 103 | self.stream.write("Hello, World!\nGoodbye, World!") 104 | 105 | self.stream.seek(7) 106 | text = self.stream.readline() 107 | self.assertEqual(text, "World!\n") 108 | self.assertEqual(self.stream.tell(), 14) 109 | 110 | text = self.stream.readline() 111 | self.assertEqual(text, "Goodbye, World!") 112 | 113 | self.stream.seek(7) 114 | text = self.stream.readline(1000) 115 | self.assertEqual(text, "World!\n") 116 | self.assertEqual(self.stream.tell(), 14) 117 | 118 | self.stream.seek(7) 119 | text = self.stream.readline(-1) 120 | self.assertEqual(text, "World!\n") 121 | self.assertEqual(self.stream.tell(), 14) 122 | 123 | self.stream.seek(7) 124 | text = self.stream.readline(5) 125 | self.assertEqual(text, "World") 126 | self.assertEqual(self.stream.tell(), 12) 127 | 128 | def test_write_read_only_failure(self): 129 | self.stream.view.set_read_only(True) 130 | 131 | self.assertRaises(ValueError, self.stream.write, 'foo') 132 | self.assertRaises(ValueError, self.stream.clear) 133 | 134 | def test_write_read_only_success(self): 135 | self.stream.view.set_read_only(True) 136 | self.stream.force_writes = True 137 | 138 | self.stream.write('foo') 139 | self.assertContents('foo') 140 | 141 | self.stream.clear() 142 | self.assertContents('') 143 | 144 | def _compare_print(self, *args, **kwargs): 145 | s = StringIO() 146 | print(*args, file=s, **kwargs) 147 | 148 | self.stream.clear() 149 | self.stream.print(*args, **kwargs) 150 | 151 | self.assertContents(s.getvalue()) 152 | 153 | self.stream.clear() 154 | print(*args, file=self.stream, **kwargs) 155 | 156 | self.assertContents(s.getvalue()) 157 | 158 | def test_print(self): 159 | text = "Hello, World!" 160 | number = 42 161 | 162 | self._compare_print(text, number) 163 | self._compare_print(text, number, sep=',', end=';') 164 | self._compare_print(text, number, sep='', end='') 165 | 166 | def test_print_no_indent(self): 167 | text = " " 168 | 169 | self.stream.view.settings().set('auto_indent', True) 170 | self.stream.print(text) 171 | self.assertContents(text + "\n") 172 | 173 | def test_print_read_only_failure(self): 174 | self.stream.view.set_read_only(True) 175 | 176 | self.assertRaises(ValueError, self.stream.print, 'foo') 177 | self.assertRaises(ValueError, self.stream.clear) 178 | 179 | def test_print_read_only_success(self): 180 | self.stream.view.set_read_only(True) 181 | self.stream.force_writes = True 182 | 183 | self.stream.print('foo') 184 | self.assertContents("foo\n") 185 | 186 | self.stream.clear() 187 | self.assertContents('') 188 | 189 | def assertSeek(self, expected, *args): 190 | returned = self.stream.seek(*args) 191 | measured = self.stream.tell() 192 | self.assertEqual(returned, expected) 193 | self.assertEqual(measured, expected) 194 | 195 | def test_seek(self): 196 | from io import SEEK_SET, SEEK_CUR, SEEK_END 197 | 198 | self.stream.write('test\n' * 10) 199 | 200 | self.assertSeek(0, -100) 201 | self.assertSeek(50, 100) 202 | self.assertSeek(25, 25, SEEK_SET) 203 | self.assertSeek(35, 10, SEEK_CUR) 204 | self.assertSeek(0, -100, SEEK_CUR) 205 | self.assertSeek(50, 0, SEEK_END) 206 | self.assertSeek(50, 100, SEEK_END) 207 | self.assertSeek(40, -10, SEEK_END) 208 | self.assertSeek(0, -100, SEEK_END) 209 | 210 | def test_seek_invalid(self): 211 | with self.assertRaises(TypeError): 212 | self.stream.seek(0, -99) 213 | 214 | def assertCursorVisible(self): 215 | self.assertTrue( 216 | self.stream.view.visible_region().contains( 217 | self.stream.tell() 218 | ) 219 | ) 220 | 221 | def test_show_cursor(self): 222 | self.stream.write('test\n' * 200) 223 | 224 | self.stream.show_cursor() 225 | yield 200 226 | self.assertCursorVisible() 227 | 228 | def test_show_cursor_auto(self): 229 | self.stream.follow_cursor = True 230 | 231 | self.stream.write('test\n' * 200) 232 | yield 200 233 | self.assertCursorVisible() 234 | 235 | self.stream.seek_start() 236 | yield 200 237 | self.assertCursorVisible() 238 | 239 | self.stream.seek_end() 240 | yield 200 241 | self.assertCursorVisible() 242 | 243 | def test_unsupported(self): 244 | self.assertRaises(UnsupportedOperation, self.stream.detach) 245 | 246 | def test_selection_guard(self): 247 | sel = self.view.sel() 248 | sel.clear() 249 | self.assertRaises(ValueError, self.stream.write, "\n") 250 | 251 | sel.add(0) 252 | self.stream.write("\n") 253 | 254 | sel.add(0) 255 | self.assertRaises(ValueError, self.stream.write, "\n") 256 | 257 | sel.clear() 258 | sel.add(sublime.Region(0, 1)) 259 | self.assertRaises(ValueError, self.stream.write, "\n") 260 | 261 | def test_validity_guard(self): 262 | self.view.set_scratch(True) 263 | self.view.close() 264 | 265 | self.assertRaises(ValueError, self.stream.read, None) 266 | self.assertRaises(ValueError, self.stream.readline) 267 | self.assertRaises(ValueError, self.stream.write, "\n") 268 | self.assertRaises(ValueError, self.stream.clear) 269 | self.assertRaises(ValueError, self.stream.seek, 0) 270 | self.assertRaises(ValueError, self.stream.seek_start) 271 | self.assertRaises(ValueError, self.stream.seek_end) 272 | self.assertRaises(ValueError, self.stream.tell) 273 | self.assertRaises(ValueError, self.stream.show_cursor) 274 | -------------------------------------------------------------------------------- /st3/sublime_lib/flags.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python enumerations for use with Sublime API methods. 3 | 4 | In addition to the standard behavior, 5 | these enumerations' constructors accept the name of an enumerated value as a string: 6 | 7 | .. code-block:: python 8 | 9 | >>> DialogResult(sublime.DIALOG_YES) 10 | 11 | >>> DialogResult("YES") 12 | 13 | 14 | Descendants of :class:`IntFlag` accept zero or more arguments: 15 | 16 | .. code-block:: python 17 | 18 | >>> PointClass("WORD_START", "WORD_END") 19 | 20 | >>> PointClass(3) 21 | 22 | >>> PointClass() 23 | 24 | 25 | .. versionchanged:: 1.2 26 | Constructors accept member names 27 | and `IntFlag` constructors accept multiple arguments. 28 | """ 29 | 30 | import sublime 31 | 32 | from ._compat.enum import IntEnum, IntFlag, EnumMeta 33 | from inspect import getdoc, cleandoc 34 | 35 | import operator 36 | import re 37 | 38 | from ._util.enum import ExtensibleConstructorMeta, construct_union, construct_with_alternatives 39 | 40 | from ._compat.typing import Callable, Optional 41 | 42 | 43 | __all__ = [ 44 | 'DialogResult', 'PointClass', 'FindOption', 'RegionOption', 45 | 'PopupOption', 'PhantomLayout', 'OpenFileOption', 'QuickPanelOption', 46 | 'HoverLocation', 'QueryContextOperator', 'CompletionOptions' 47 | ] 48 | 49 | 50 | def autodoc(prefix: Optional[str] = None) -> Callable[[EnumMeta], EnumMeta]: 51 | if prefix is None: 52 | prefix_str = '' 53 | else: 54 | prefix_str = prefix + '_' 55 | 56 | def decorator(enum: EnumMeta) -> EnumMeta: 57 | enum.__doc__ = (getdoc(enum) or '') + '\n\n' + '\n'.join([ 58 | cleandoc(""" 59 | .. py:attribute:: {name} 60 | :annotation: = sublime.{pre}{name} 61 | """).format(name=name, pre=prefix_str) for name in enum.__members__ 62 | ]) 63 | 64 | return enum 65 | 66 | return decorator 67 | 68 | 69 | construct_from_name = construct_with_alternatives( 70 | lambda cls, value: cls.__members__.get(value, None) 71 | ) 72 | 73 | 74 | @autodoc('DIALOG') 75 | @construct_from_name 76 | class DialogResult(IntEnum): 77 | """ 78 | An :class:`~enum.IntEnum` for use with :func:`sublime.yes_no_cancel_dialog`. 79 | """ 80 | CANCEL = sublime.DIALOG_CANCEL 81 | YES = sublime.DIALOG_YES 82 | NO = sublime.DIALOG_NO 83 | 84 | 85 | @autodoc('CLASS') 86 | @construct_union 87 | @construct_from_name 88 | class PointClass(IntFlag, metaclass=ExtensibleConstructorMeta): 89 | """ 90 | An :class:`~enum.IntFlag` for use with several methods of :class:`sublime.View`: 91 | 92 | - :meth:`~sublime.View.classify` 93 | - :meth:`~sublime.View.find_by_class` 94 | - :meth:`~sublime.View.expand_by_class` 95 | """ 96 | WORD_START = sublime.CLASS_WORD_START 97 | WORD_END = sublime.CLASS_WORD_END 98 | PUNCTUATION_START = sublime.CLASS_PUNCTUATION_START 99 | PUNCTUATION_END = sublime.CLASS_PUNCTUATION_END 100 | SUB_WORD_START = sublime.CLASS_SUB_WORD_START 101 | SUB_WORD_END = sublime.CLASS_SUB_WORD_END 102 | LINE_START = sublime.CLASS_LINE_START 103 | LINE_END = sublime.CLASS_LINE_END 104 | EMPTY_LINE = sublime.CLASS_EMPTY_LINE 105 | 106 | 107 | @autodoc() 108 | @construct_union 109 | @construct_from_name 110 | class FindOption(IntFlag, metaclass=ExtensibleConstructorMeta): 111 | """ 112 | An :class:`~enum.IntFlag` for use with several methods of :class:`sublime.View`: 113 | 114 | - :meth:`~sublime.View.find` 115 | - :meth:`~sublime.View.find_all` 116 | """ 117 | LITERAL = sublime.LITERAL 118 | IGNORECASE = sublime.IGNORECASE 119 | 120 | 121 | @autodoc() 122 | @construct_union 123 | @construct_from_name 124 | class RegionOption(IntFlag, metaclass=ExtensibleConstructorMeta): 125 | """ 126 | An :class:`~enum.IntFlag` for use with :meth:`sublime.View.add_regions`. 127 | """ 128 | DRAW_EMPTY = sublime.DRAW_EMPTY 129 | HIDE_ON_MINIMAP = sublime.HIDE_ON_MINIMAP 130 | DRAW_EMPTY_AS_OVERWRITE = sublime.DRAW_EMPTY_AS_OVERWRITE 131 | DRAW_NO_FILL = sublime.DRAW_NO_FILL 132 | DRAW_NO_OUTLINE = sublime.DRAW_NO_OUTLINE 133 | DRAW_SOLID_UNDERLINE = sublime.DRAW_SOLID_UNDERLINE 134 | DRAW_STIPPLED_UNDERLINE = sublime.DRAW_STIPPLED_UNDERLINE 135 | DRAW_SQUIGGLY_UNDERLINE = sublime.DRAW_SQUIGGLY_UNDERLINE 136 | PERSISTENT = sublime.PERSISTENT 137 | HIDDEN = sublime.HIDDEN 138 | 139 | 140 | @autodoc() 141 | @construct_union 142 | @construct_from_name 143 | class PopupOption(IntFlag, metaclass=ExtensibleConstructorMeta): 144 | """ 145 | An :class:`~enum.IntFlag` for use with :meth:`sublime.View.show_popup`. 146 | """ 147 | COOPERATE_WITH_AUTO_COMPLETE = sublime.COOPERATE_WITH_AUTO_COMPLETE 148 | HIDE_ON_MOUSE_MOVE = sublime.HIDE_ON_MOUSE_MOVE 149 | HIDE_ON_MOUSE_MOVE_AWAY = sublime.HIDE_ON_MOUSE_MOVE_AWAY 150 | 151 | 152 | @autodoc('LAYOUT') 153 | @construct_union 154 | @construct_from_name 155 | class PhantomLayout(IntFlag, metaclass=ExtensibleConstructorMeta): 156 | """ 157 | An :class:`~enum.IntFlag` for use with :class:`sublime.Phantom`. 158 | """ 159 | INLINE = sublime.LAYOUT_INLINE 160 | BELOW = sublime.LAYOUT_BELOW 161 | BLOCK = sublime.LAYOUT_BLOCK 162 | 163 | 164 | @autodoc() 165 | @construct_union 166 | @construct_from_name 167 | class OpenFileOption(IntFlag, metaclass=ExtensibleConstructorMeta): 168 | """ 169 | An :class:`~enum.IntFlag` for use with :meth:`sublime.Window.open_file`. 170 | """ 171 | ENCODED_POSITION = sublime.ENCODED_POSITION 172 | TRANSIENT = sublime.TRANSIENT 173 | FORCE_GROUP = sublime.FORCE_GROUP 174 | 175 | 176 | @autodoc() 177 | @construct_union 178 | @construct_from_name 179 | class QuickPanelOption(IntFlag, metaclass=ExtensibleConstructorMeta): 180 | """ 181 | An :class:`~enum.IntFlag` for use with :meth:`sublime.Window.show_quick_panel`. 182 | """ 183 | MONOSPACE_FONT = sublime.MONOSPACE_FONT 184 | KEEP_OPEN_ON_FOCUS_LOST = sublime.KEEP_OPEN_ON_FOCUS_LOST 185 | 186 | 187 | @autodoc('HOVER') 188 | @construct_from_name 189 | class HoverLocation(IntEnum): 190 | """ 191 | An :class:`~enum.IntEnum` for use with 192 | :func:`sublime_plugin.EventListener.on_hover`. 193 | 194 | .. versionadded:: 1.4 195 | """ 196 | TEXT = sublime.HOVER_TEXT 197 | GUTTER = sublime.HOVER_GUTTER 198 | MARGIN = sublime.HOVER_MARGIN 199 | 200 | 201 | def regex_match(value: str, operand: str) -> bool: 202 | expr = r'(?:{})\Z'.format(operand) 203 | return re.match(expr, value) is not None 204 | 205 | 206 | def not_regex_match(value: str, operand: str) -> bool: 207 | return not regex_match(value, operand) 208 | 209 | 210 | def regex_contains(value: str, operand: str) -> bool: 211 | return re.search(operand, value) is not None 212 | 213 | 214 | def not_regex_contains(value: str, operand: str) -> bool: 215 | return not regex_contains(value, operand) 216 | 217 | 218 | @autodoc('OP') 219 | @construct_from_name 220 | class QueryContextOperator(IntEnum): 221 | """ 222 | An :class:`~enum.IntEnum` for use with 223 | :func:`sublime_plugin.EventListener.on_query_context`. 224 | 225 | .. versionadded:: 1.4 226 | 227 | .. py:method:: apply(value, operand) 228 | 229 | Apply the operation to the given values. 230 | 231 | For regexp operators, 232 | `operand` should contain the regexp to be tested against the string `value`. 233 | 234 | Example usage: 235 | 236 | .. code-block:: python 237 | 238 | import sublime_plugin 239 | from sublime_lib.flags import QueryContextOperator 240 | 241 | class MyListener(sublime_plugin.EventListener): 242 | def on_query_context(self, view, key, operator, operand, match_all): 243 | if key == "my_example_key": 244 | value = get_some_value() 245 | return QueryContextOperator(operator).apply(value, operand) 246 | else: 247 | return None 248 | """ 249 | EQUAL = (sublime.OP_EQUAL, operator.eq) 250 | NOT_EQUAL = (sublime.OP_NOT_EQUAL, operator.ne) 251 | REGEX_MATCH = (sublime.OP_REGEX_MATCH, regex_match) 252 | NOT_REGEX_MATCH = (sublime.OP_NOT_REGEX_MATCH, not_regex_match) 253 | REGEX_CONTAINS = (sublime.OP_REGEX_CONTAINS, regex_contains) 254 | NOT_REGEX_CONTAINS = (sublime.OP_NOT_REGEX_CONTAINS, not_regex_contains) 255 | 256 | # _apply_ = None # type: Callable[[str, str], bool] 257 | 258 | def __new__(cls, value: int, operator: Callable[[str, str], bool]) -> 'QueryContextOperator': 259 | obj = int.__new__(cls, value) # type: ignore 260 | obj._value_ = value 261 | obj._apply_ = operator 262 | return obj 263 | 264 | def apply(self, value: str, operand: str) -> bool: 265 | return self._apply_(value, operand) # type: ignore 266 | 267 | 268 | @autodoc() 269 | @construct_union 270 | @construct_from_name 271 | class CompletionOptions(IntFlag, metaclass=ExtensibleConstructorMeta): 272 | """ 273 | An :class:`~enum.IntFlag` for use with 274 | :func:`sublime_plugin.EventListener.on_query_completions`. 275 | 276 | .. versionadded:: 1.4 277 | """ 278 | INHIBIT_WORD_COMPLETIONS = sublime.INHIBIT_WORD_COMPLETIONS 279 | INHIBIT_EXPLICIT_COMPLETIONS = sublime.INHIBIT_EXPLICIT_COMPLETIONS 280 | -------------------------------------------------------------------------------- /tests/test_selection_panel.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from sublime_lib import show_selection_panel 3 | 4 | from unittest import TestCase 5 | from unittest.mock import NonCallableMagicMock, MagicMock 6 | 7 | from inspect import signature 8 | 9 | 10 | class WindowMock(NonCallableMagicMock): 11 | def __init__(self, callback=None): 12 | super().__init__() 13 | 14 | def side_effect(*args, **kwargs): 15 | bound_args = (signature(sublime.Window.show_quick_panel) 16 | .bind(self, *args, **kwargs).arguments) 17 | callback(**bound_args) 18 | 19 | self.show_quick_panel = MagicMock( 20 | spec=sublime.Window.show_quick_panel, 21 | side_effect=side_effect if callback else None 22 | ) 23 | 24 | 25 | # From Python 3.5 26 | # https://github.com/python/cpython/blob/master/Lib/unittest/mock.py 27 | def assert_not_called(_mock_self): 28 | """assert that the mock was never called. 29 | """ 30 | self = _mock_self 31 | if self.call_count != 0: 32 | msg = ("Expected '%s' to not have been called. Called %s times.%s" 33 | % (self._mock_name or 'mock', 34 | self.call_count, 35 | self._calls_repr())) 36 | raise AssertionError(msg) 37 | 38 | 39 | # From Python 3.5 40 | def assert_called_once(_mock_self): 41 | """assert that the mock was called only once. 42 | """ 43 | self = _mock_self 44 | if not self.call_count == 1: 45 | msg = ("Expected '%s' to have been called once. Called %s times.%s" 46 | % (self._mock_name or 'mock', 47 | self.call_count, 48 | self._calls_repr())) 49 | raise AssertionError(msg) 50 | 51 | 52 | def assert_called_once_with_partial(mock, **specified_args): 53 | """Assert that the mock was called once with the specified args, and 54 | optionally with other args.""" 55 | assert_called_once(mock) 56 | called_args, called_kwargs = mock.call_args 57 | called_dict = (signature(sublime.Window.show_quick_panel) 58 | .bind(None, *called_args, **called_kwargs).arguments) 59 | 60 | failures = any( 61 | k in specified_args and v != specified_args[k] 62 | for k, v in called_dict.items() 63 | ) 64 | 65 | if failures: 66 | mock.assert_called_once_with(**specified_args) 67 | 68 | 69 | class TestSelectionPanel(TestCase): 70 | def test_type_coercion(self): 71 | window = WindowMock() 72 | show_selection_panel( 73 | window=window, 74 | items=[10, 20, 30], 75 | ) 76 | 77 | assert_called_once_with_partial( 78 | window.show_quick_panel, 79 | items=[['10'], ['20'], ['30']], 80 | ) 81 | 82 | def test_multiline_type_coercion(self): 83 | window = WindowMock() 84 | show_selection_panel( 85 | window=window, 86 | items=[ 87 | ['a', 10], 88 | ['b', 20], 89 | ('c', 30), 90 | ], 91 | ) 92 | 93 | assert_called_once_with_partial( 94 | window.show_quick_panel, 95 | items=[ 96 | ['a', '10'], 97 | ['b', '20'], 98 | ['c', '30'], 99 | ], 100 | ) 101 | 102 | def test_empty_items_error(self): 103 | with self.assertRaises(ValueError): 104 | show_selection_panel( 105 | WindowMock(), 106 | items=[] 107 | ) 108 | 109 | def test_mixed_label_types(self): 110 | window = WindowMock() 111 | show_selection_panel( 112 | window=window, 113 | items=['a', 'b'], 114 | labels=[['a'], 'b'] 115 | ) 116 | 117 | assert_called_once_with_partial( 118 | window.show_quick_panel, 119 | items=[['a'], ['b']], 120 | ) 121 | 122 | def test_mixed_label_lengths(self): 123 | window = WindowMock() 124 | show_selection_panel( 125 | window=window, 126 | items=['a', 'b'], 127 | labels=[['a'], ['b', 'c']] 128 | ) 129 | 130 | assert_called_once_with_partial( 131 | window.show_quick_panel, 132 | items=[['a', ''], ['b', 'c']], 133 | ) 134 | 135 | def test_item_labels_lengths_error(self): 136 | with self.assertRaises(ValueError): 137 | show_selection_panel( 138 | WindowMock(), 139 | items=['a', 'b', 'c'], 140 | labels=['x', 'y'] 141 | ) 142 | 143 | def test_selected(self): 144 | on_select = MagicMock() 145 | on_cancel = MagicMock() 146 | 147 | show_selection_panel( 148 | window=WindowMock(lambda on_select, **rest: on_select(1)), 149 | items=['a', 'b', 'c'], 150 | on_select=on_select, 151 | on_cancel=on_cancel 152 | ) 153 | on_select.assert_called_once_with('b') 154 | assert_not_called(on_cancel) 155 | 156 | def test_cancel(self): 157 | on_select = MagicMock() 158 | on_cancel = MagicMock() 159 | 160 | show_selection_panel( 161 | window=WindowMock(lambda on_select, **rest: on_select(-1)), 162 | items=['a', 'b', 'c'], 163 | on_select=on_select, 164 | on_cancel=on_cancel 165 | ) 166 | assert_not_called(on_select) 167 | on_cancel.assert_called_once_with() 168 | 169 | def test_highlight(self): 170 | on_highlight = MagicMock() 171 | 172 | show_selection_panel( 173 | window=WindowMock(lambda on_highlight, **rest: on_highlight(1)), 174 | items=['a', 'b', 'c'], 175 | on_highlight=on_highlight, 176 | ) 177 | on_highlight.assert_called_once_with('b') 178 | 179 | def test_no_flags(self): 180 | window = WindowMock() 181 | 182 | show_selection_panel( 183 | window=window, 184 | items=['a', 'b', 'c'] 185 | ) 186 | 187 | assert_called_once_with_partial( 188 | window.show_quick_panel, 189 | flags=0 190 | ) 191 | 192 | def test_flags(self): 193 | window = WindowMock() 194 | 195 | flags = sublime.MONOSPACE_FONT | sublime.KEEP_OPEN_ON_FOCUS_LOST 196 | 197 | show_selection_panel( 198 | window=window, 199 | items=['a', 'b', 'c'], 200 | flags=flags 201 | ) 202 | 203 | assert_called_once_with_partial( 204 | window.show_quick_panel, 205 | flags=flags 206 | ) 207 | 208 | def test_flags_conversion(self): 209 | window = WindowMock() 210 | 211 | flags = ['MONOSPACE_FONT', 'KEEP_OPEN_ON_FOCUS_LOST'] 212 | 213 | show_selection_panel( 214 | window=window, 215 | items=['a', 'b', 'c'], 216 | flags=flags 217 | ) 218 | 219 | assert_called_once_with_partial( 220 | window.show_quick_panel, 221 | flags=(sublime.MONOSPACE_FONT | sublime.KEEP_OPEN_ON_FOCUS_LOST) 222 | ) 223 | 224 | def test_labels_function(self): 225 | window = WindowMock() 226 | 227 | show_selection_panel( 228 | window=window, 229 | items=[ 230 | {'name': 'a'}, 231 | {'name': 'b'}, 232 | {'name': 'c'}, 233 | ], 234 | labels=lambda item: item['name'] 235 | ) 236 | 237 | assert_called_once_with_partial( 238 | window.show_quick_panel, 239 | items=[['a'], ['b'], ['c']], 240 | ) 241 | 242 | def test_labels_list(self): 243 | window = WindowMock() 244 | 245 | show_selection_panel( 246 | window=window, 247 | items=[ 248 | {'name': 'a'}, 249 | {'name': 'b'}, 250 | {'name': 'c'}, 251 | ], 252 | labels=['a', 'b', 'c'], 253 | ) 254 | 255 | assert_called_once_with_partial( 256 | window.show_quick_panel, 257 | items=[['a'], ['b'], ['c']], 258 | ) 259 | 260 | def test_no_selected(self): 261 | window = WindowMock() 262 | 263 | show_selection_panel( 264 | window=window, 265 | items=['a', 'b', 'c'], 266 | ) 267 | 268 | assert_called_once_with_partial( 269 | window.show_quick_panel, 270 | selected_index=-1 271 | ) 272 | 273 | def test_selected_simple(self): 274 | window = WindowMock() 275 | 276 | show_selection_panel( 277 | window=window, 278 | items=['a', 'b', 'c'], 279 | selected='b' 280 | ) 281 | 282 | assert_called_once_with_partial( 283 | window.show_quick_panel, 284 | selected_index=1 285 | ) 286 | 287 | def test_selected_complex(self): 288 | window = WindowMock() 289 | 290 | show_selection_panel( 291 | window=window, 292 | items=[ 293 | {'name': 'a'}, 294 | {'name': 'b'}, 295 | {'name': 'c'}, 296 | ], 297 | labels=lambda item: item['name'], 298 | selected={'name': 'b'} 299 | ) 300 | 301 | assert_called_once_with_partial( 302 | window.show_quick_panel, 303 | selected_index=1 304 | ) 305 | 306 | def test_selected_invalid(self): 307 | window = WindowMock() 308 | 309 | self.assertRaises( 310 | ValueError, 311 | show_selection_panel, 312 | window=window, 313 | items=[], 314 | selected='b' 315 | ) 316 | -------------------------------------------------------------------------------- /tests/test_pure_resource_path.py: -------------------------------------------------------------------------------- 1 | from sublime_lib import ResourcePath 2 | 3 | from unittest import TestCase 4 | 5 | 6 | class TestPureResourcePath(TestCase): 7 | 8 | def test_empty_error(self): 9 | with self.assertRaises(ValueError): 10 | ResourcePath("") 11 | 12 | def test_eq(self): 13 | self.assertEqual( 14 | ResourcePath("Packages/Foo/bar.py"), 15 | ResourcePath("Packages/Foo/bar.py") 16 | ) 17 | 18 | def test_ordering_error(self): 19 | with self.assertRaises(TypeError): 20 | ResourcePath("Packages") < 'Packages' 21 | 22 | def test_hash(self): 23 | self.assertIsInstance( 24 | hash(ResourcePath("Packages/Foo/bar.py")), 25 | int 26 | ) 27 | 28 | def test_eq_false(self): 29 | self.assertNotEqual( 30 | ResourcePath("Packages/Foo/bar.py"), 31 | "Packages/Foo/bar.py" 32 | ) 33 | 34 | def test_eq_slash(self): 35 | self.assertEqual( 36 | ResourcePath("Packages/Foo/bar.py"), 37 | ResourcePath("Packages/Foo/bar.py///") 38 | ) 39 | 40 | def test_str(self): 41 | self.assertEqual( 42 | str(ResourcePath("Packages/Foo/bar.py")), 43 | "Packages/Foo/bar.py" 44 | ) 45 | 46 | def test_repr(self): 47 | self.assertEqual( 48 | repr(ResourcePath("Packages/Foo/bar.py")), 49 | "ResourcePath('Packages/Foo/bar.py')" 50 | ) 51 | 52 | def test_parts(self): 53 | path = ResourcePath("Packages/Foo/bar.py") 54 | self.assertEqual(path.parts, ("Packages", "Foo", "bar.py")) 55 | 56 | def test_parent(self): 57 | self.assertEqual( 58 | ResourcePath("Packages/Foo/bar.py").parent, 59 | ResourcePath("Packages/Foo") 60 | ) 61 | 62 | def test_top_parent(self): 63 | self.assertEqual( 64 | ResourcePath("Packages").parent, 65 | ResourcePath("Packages") 66 | ) 67 | 68 | def test_parents(self): 69 | self.assertEqual( 70 | ResourcePath("Packages/Foo/bar.py").parents, 71 | ( 72 | ResourcePath("Packages/Foo"), 73 | ResourcePath("Packages") 74 | ) 75 | ) 76 | 77 | def test_parents_root(self): 78 | self.assertEqual( 79 | ResourcePath("Packages").parents, 80 | () 81 | ) 82 | 83 | def test_name(self): 84 | self.assertEqual( 85 | ResourcePath("Packages/Foo/bar.py").name, 86 | 'bar.py' 87 | ) 88 | 89 | def test_name_directory(self): 90 | self.assertEqual( 91 | ResourcePath("Packages/Foo/").name, 92 | 'Foo' 93 | ) 94 | 95 | def test_suffix(self): 96 | self.assertEqual( 97 | ResourcePath("Packages/Foo/bar.py").suffix, 98 | '.py' 99 | ) 100 | 101 | def test_suffix_none(self): 102 | self.assertEqual( 103 | ResourcePath("Packages/Foo/bar").suffix, 104 | '' 105 | ) 106 | 107 | def test_suffix_dots_end(self): 108 | self.assertEqual( 109 | ResourcePath("foo...").suffix, 110 | "" 111 | ) 112 | 113 | def test_suffix_multiple(self): 114 | self.assertEqual( 115 | ResourcePath("Packages/Foo/bar.tar.gz").suffix, 116 | '.gz' 117 | ) 118 | 119 | def test_suffixes(self): 120 | self.assertEqual( 121 | ResourcePath("Packages/Foo/bar.tar.gz").suffixes, 122 | ['.tar', '.gz'] 123 | ) 124 | 125 | def test_suffixes_none(self): 126 | self.assertEqual( 127 | ResourcePath("Packages/Foo/bar").suffixes, 128 | [] 129 | ) 130 | 131 | def test_suffixes_dotend(self): 132 | self.assertEqual( 133 | ResourcePath("foo.bar.").suffixes, 134 | [] 135 | ) 136 | 137 | def test_suffixes_dots(self): 138 | self.assertEqual( 139 | ResourcePath("foo.bar...baz").suffixes, 140 | ['.bar', '.', '.', '.baz'] 141 | ) 142 | 143 | def test_stem(self): 144 | self.assertEqual( 145 | ResourcePath("Packages/Foo/bar.py").stem, 146 | 'bar' 147 | ) 148 | 149 | def test_stem_dots_end(self): 150 | self.assertEqual( 151 | ResourcePath("foo...").stem, 152 | "foo..." 153 | ) 154 | 155 | def test_stem_multiple(self): 156 | self.assertEqual( 157 | ResourcePath("Packages/Foo/bar.tar.gz").stem, 158 | 'bar.tar' 159 | ) 160 | 161 | def test_stem_none(self): 162 | self.assertEqual( 163 | ResourcePath("Packages/Foo/bar").stem, 164 | 'bar' 165 | ) 166 | 167 | def test_root(self): 168 | self.assertEqual( 169 | ResourcePath("Packages/Foo/bar").root, 170 | 'Packages' 171 | ) 172 | 173 | def test_package(self): 174 | self.assertEqual( 175 | ResourcePath("Packages/Foo/bar").package, 176 | 'Foo' 177 | ) 178 | 179 | def test_package_none(self): 180 | self.assertEqual( 181 | ResourcePath("Packages").package, 182 | None 183 | ) 184 | 185 | def test_package_cache(self): 186 | self.assertEqual( 187 | ResourcePath("Cache/Foo").package, 188 | 'Foo' 189 | ) 190 | 191 | def test_match(self): 192 | path = ResourcePath("Packages/Foo/bar") 193 | 194 | self.assertTrue(path.match('bar')) 195 | self.assertTrue(path.match('Foo/bar')) 196 | self.assertTrue(path.match('Foo/*')) 197 | self.assertTrue(path.match('Packages/*/bar')) 198 | self.assertTrue(path.match('Packages/Foo/**/bar')) 199 | self.assertTrue(path.match("/Packages/Foo/bar")) 200 | 201 | self.assertFalse(path.match('baz')) 202 | self.assertFalse(path.match('Foo')) 203 | self.assertFalse(path.match('Packages/*/*/bar')) 204 | self.assertFalse(path.match('/Foo/bar')) 205 | self.assertFalse(path.match('ar')) 206 | 207 | def test_joinpath(self): 208 | self.assertEqual( 209 | ResourcePath("Packages/Foo/").joinpath('bar/', 'baz/xyzzy'), 210 | ResourcePath("Packages/Foo/bar/baz/xyzzy") 211 | ) 212 | 213 | def test_joinpath_operator(self): 214 | self.assertEqual( 215 | ResourcePath("Packages/Foo/") / 'bar/' / 'baz/xyzzy', 216 | ResourcePath("Packages/Foo/bar/baz/xyzzy") 217 | ) 218 | 219 | def test_relative_to(self): 220 | self.assertEqual( 221 | ResourcePath("Packages/Foo/baz/bar.py").relative_to( 222 | ResourcePath("Packages/Foo") 223 | ), 224 | ('baz', 'bar.py') 225 | ) 226 | 227 | def test_relative_to_same(self): 228 | self.assertEqual( 229 | ResourcePath("Packages/Foo").relative_to( 230 | ResourcePath("Packages/Foo") 231 | ), 232 | () 233 | ) 234 | 235 | def test_relative_to_error(self): 236 | with self.assertRaises(ValueError): 237 | ResourcePath("Packages/Foo").relative_to( 238 | ResourcePath("Packages/Bar") 239 | ) 240 | 241 | def test_with_name(self): 242 | self.assertEqual( 243 | ResourcePath("Packages/Foo/bar.py").with_name('baz.js'), 244 | ResourcePath("Packages/Foo/baz.js") 245 | ) 246 | 247 | def test_with_name_root(self): 248 | self.assertEqual( 249 | ResourcePath("Packages").with_name('Cache'), 250 | ResourcePath("Cache") 251 | ) 252 | 253 | def test_add_suffix(self): 254 | self.assertEqual( 255 | ResourcePath("Packages/Foo/bar").add_suffix('.py'), 256 | ResourcePath("Packages/Foo/bar.py") 257 | ) 258 | 259 | def test_remove_suffix(self): 260 | self.assertEqual( 261 | ResourcePath("Packages/Foo/bar.py").remove_suffix(), 262 | ResourcePath("Packages/Foo/bar") 263 | ) 264 | 265 | def test_remove_suffix_none(self): 266 | self.assertEqual( 267 | ResourcePath("Packages/Foo/bar").remove_suffix(must_remove=False), 268 | ResourcePath("Packages/Foo/bar") 269 | ) 270 | 271 | def test_remove_suffix_none_error(self): 272 | with self.assertRaises(ValueError): 273 | ResourcePath("Packages/Foo/bar").remove_suffix() 274 | 275 | def test_remove_suffix_specified(self): 276 | self.assertEqual( 277 | ResourcePath("Packages/Foo/bar.py").remove_suffix('.py'), 278 | ResourcePath("Packages/Foo/bar") 279 | ) 280 | 281 | def test_remove_suffix_specified_no_match(self): 282 | self.assertEqual( 283 | ResourcePath("Packages/Foo/bar.py").remove_suffix('.zip', must_remove=False), 284 | ResourcePath("Packages/Foo/bar.py") 285 | ) 286 | 287 | def test_remove_suffix_specified_no_match_error(self): 288 | with self.assertRaises(ValueError): 289 | ResourcePath("Packages/Foo/bar.py").remove_suffix('.zip') 290 | 291 | def test_remove_suffix_specified_no_dot(self): 292 | self.assertEqual( 293 | ResourcePath("Packages/Foo/bar.py").remove_suffix('r.py'), 294 | ResourcePath("Packages/Foo/ba") 295 | ) 296 | 297 | def test_remove_suffix_specified_entire_name(self): 298 | self.assertEqual( 299 | ResourcePath("Packages/Foo/bar.py").remove_suffix('bar.py', must_remove=False), 300 | ResourcePath("Packages/Foo/bar.py") 301 | ) 302 | 303 | def test_remove_suffix_specified_entire_name_error(self): 304 | with self.assertRaises(ValueError): 305 | ResourcePath("Packages/Foo/bar.py").remove_suffix('bar.py') 306 | 307 | def test_remove_suffix_multiple(self): 308 | self.assertEqual( 309 | ResourcePath("Packages/Foo/bar.py").remove_suffix(['.zip', '.py']), 310 | ResourcePath("Packages/Foo/bar") 311 | ) 312 | 313 | def test_remove_suffix_multiple_matches(self): 314 | self.assertEqual( 315 | ResourcePath("Packages/Foo/bar.tar.gz").remove_suffix(['.tar.gz', '.gz']), 316 | ResourcePath("Packages/Foo/bar") 317 | ) 318 | 319 | def test_remove_suffix_multiple_matches_backward(self): 320 | self.assertEqual( 321 | ResourcePath("Packages/Foo/bar.tar.gz").remove_suffix(['.gz', '.tar.gz']), 322 | ResourcePath("Packages/Foo/bar") 323 | ) 324 | 325 | def test_with_suffix(self): 326 | self.assertEqual( 327 | ResourcePath("Packages/Foo/bar.tar.gz").with_suffix('.bz2'), 328 | ResourcePath("Packages/Foo/bar.tar.bz2") 329 | ) 330 | 331 | def test_with_suffix_empty(self): 332 | self.assertEqual( 333 | ResourcePath("Packages/Foo/bar").with_suffix('.py'), 334 | ResourcePath("Packages/Foo/bar.py") 335 | ) 336 | 337 | def test_with_suffix_remove(self): 338 | self.assertEqual( 339 | ResourcePath("Packages/Foo/bar.py").with_suffix(''), 340 | ResourcePath("Packages/Foo/bar") 341 | ) 342 | 343 | def test_with_suffix_root(self): 344 | self.assertEqual( 345 | ResourcePath("Packages").with_suffix('.bz2'), 346 | ResourcePath("Packages.bz2") 347 | ) 348 | -------------------------------------------------------------------------------- /tests/test_resource_path.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import tempfile 3 | 4 | from sublime_lib import ResourcePath 5 | from sublime_lib._compat.pathlib import Path 6 | from .temporary_package import TemporaryPackage 7 | 8 | from unittesting import DeferrableTestCase 9 | 10 | 11 | class TestResourcePath(DeferrableTestCase): 12 | 13 | def setUp(self): 14 | self.temp = TemporaryPackage( 15 | 'test_package', 16 | ResourcePath("Packages/sublime_lib/tests/test_package") 17 | ) 18 | self.temp.create() 19 | 20 | yield self.temp.exists 21 | 22 | def tearDown(self): 23 | self.temp.destroy() 24 | 25 | def test_glob_resources(self): 26 | self.assertEqual( 27 | ResourcePath.glob_resources("Packages/test_package/*.txt"), 28 | [ 29 | ResourcePath("Packages/test_package/helloworld.txt"), 30 | ResourcePath("Packages/test_package/UTF-8-test.txt"), 31 | ] 32 | ) 33 | 34 | self.assertEqual( 35 | ResourcePath.glob_resources("ks27jArEz4"), 36 | [] 37 | ) 38 | 39 | self.assertEqual( 40 | ResourcePath.glob_resources("*ks27jArEz4"), 41 | [ 42 | ResourcePath('Packages/sublime_lib/tests/uniquely_named_file_ks27jArEz4') 43 | ] 44 | ) 45 | 46 | def test_from_file_path_packages(self): 47 | self.assertEqual( 48 | ResourcePath.from_file_path(Path(sublime.packages_path(), 'test_package')), 49 | ResourcePath("Packages/test_package") 50 | ) 51 | 52 | def test_from_file_path_cache(self): 53 | self.assertEqual( 54 | ResourcePath.from_file_path(Path(sublime.cache_path(), 'test_package')), 55 | ResourcePath("Cache/test_package") 56 | ) 57 | 58 | def test_from_file_path_installed_packages(self): 59 | self.assertEqual( 60 | ResourcePath.from_file_path( 61 | Path(sublime.installed_packages_path(), 'test_package.sublime-package', 'foo.py') 62 | ), 63 | ResourcePath("Packages/test_package/foo.py") 64 | ) 65 | 66 | def test_from_file_path_installed_packages_not_installed(self): 67 | with self.assertRaises(ValueError): 68 | ResourcePath.from_file_path( 69 | Path(sublime.installed_packages_path(), 'test_package', 'foo.py') 70 | ), 71 | 72 | def test_from_file_path_installed_packages_root(self): 73 | self.assertEqual( 74 | ResourcePath.from_file_path(Path(sublime.installed_packages_path())), 75 | ResourcePath("Packages") 76 | ) 77 | 78 | def test_from_file_path_default_packages(self): 79 | self.assertEqual( 80 | ResourcePath.from_file_path( 81 | Path(sublime.executable_path()).parent.joinpath( 82 | 'Packages', 'test_package.sublime-package', 'foo.py' 83 | ) 84 | ), 85 | ResourcePath("Packages/test_package/foo.py") 86 | ) 87 | 88 | def test_from_file_path_default_packages_root(self): 89 | self.assertEqual( 90 | ResourcePath.from_file_path( 91 | Path(sublime.executable_path()).parent / 'Packages' 92 | ), 93 | ResourcePath("Packages") 94 | ) 95 | 96 | def test_from_file_path_error(self): 97 | with self.assertRaises(ValueError): 98 | ResourcePath.from_file_path(Path('/test_package')), 99 | 100 | def test_from_file_path_relative(self): 101 | with self.assertRaises(ValueError): 102 | ResourcePath.from_file_path(Path('test_package')), 103 | 104 | def test_file_path_packages(self): 105 | self.assertEqual( 106 | ResourcePath("Packages/Foo/bar.py").file_path(), 107 | Path(sublime.packages_path(), 'Foo/bar.py') 108 | ) 109 | 110 | def test_file_path_packages_root(self): 111 | self.assertEqual( 112 | ResourcePath("Packages").file_path(), 113 | Path(sublime.packages_path()) 114 | ) 115 | 116 | def test_file_path_cache(self): 117 | self.assertEqual( 118 | ResourcePath("Cache/Foo/bar.py").file_path(), 119 | Path(sublime.cache_path(), 'Foo/bar.py') 120 | ) 121 | 122 | def test_file_path_error(self): 123 | with self.assertRaises(ValueError): 124 | ResourcePath("Elsewhere/Foo/bar.py").file_path(), 125 | 126 | def test_exists(self): 127 | self.assertTrue( 128 | ResourcePath("Packages/test_package/helloworld.txt").exists() 129 | ) 130 | 131 | def test_not_exists(self): 132 | self.assertFalse( 133 | ResourcePath("Packages/test_package/nonexistentfile.txt").exists() 134 | ) 135 | 136 | def test_read_text(self): 137 | self.assertEqual( 138 | ResourcePath("Packages/test_package/helloworld.txt").read_text(), 139 | "Hello, World!\n" 140 | ) 141 | 142 | def test_read_text_missing(self): 143 | with self.assertRaises(FileNotFoundError): 144 | ResourcePath("Packages/test_package/nonexistentfile.txt").read_text() 145 | 146 | def test_read_text_invalid_unicode(self): 147 | with self.assertRaises(UnicodeDecodeError): 148 | ResourcePath("Packages/test_package/UTF-8-test.txt").read_text() 149 | 150 | def test_read_bytes(self): 151 | self.assertEqual( 152 | ResourcePath("Packages/test_package/helloworld.txt").read_bytes(), 153 | b"Hello, World!\n" 154 | ) 155 | 156 | def test_read_bytes_missing(self): 157 | with self.assertRaises(FileNotFoundError): 158 | ResourcePath("Packages/test_package/nonexistentfile.txt").read_bytes() 159 | 160 | def test_read_bytes_invalid_unicode(self): 161 | # Should not raise UnicodeDecodeError 162 | ResourcePath("Packages/test_package/UTF-8-test.txt").read_bytes() 163 | 164 | def test_glob(self): 165 | self.assertEqual( 166 | ResourcePath("Packages/test_package").glob('*.txt'), 167 | [ 168 | ResourcePath("Packages/test_package/helloworld.txt"), 169 | ResourcePath("Packages/test_package/UTF-8-test.txt"), 170 | ] 171 | ) 172 | 173 | def test_rglob(self): 174 | self.assertEqual( 175 | ResourcePath("Packages/test_package").rglob('*.txt'), 176 | [ 177 | ResourcePath("Packages/test_package/helloworld.txt"), 178 | ResourcePath("Packages/test_package/UTF-8-test.txt"), 179 | ResourcePath("Packages/test_package/directory/goodbyeworld.txt"), 180 | ] 181 | ) 182 | 183 | def test_rglob_error(self): 184 | with self.assertRaises(NotImplementedError): 185 | ResourcePath("Packages/test_package").rglob('/*.txt') 186 | 187 | def test_children(self): 188 | self.assertEqual( 189 | ResourcePath("Packages/test_package").children(), 190 | [ 191 | ResourcePath("Packages/test_package/.test_package_exists"), 192 | ResourcePath("Packages/test_package/helloworld.txt"), 193 | ResourcePath("Packages/test_package/UTF-8-test.txt"), 194 | ResourcePath("Packages/test_package/directory"), 195 | ] 196 | ) 197 | 198 | def test_copy_text(self): 199 | with tempfile.TemporaryDirectory() as directory: 200 | source = ResourcePath("Packages/test_package/helloworld.txt") 201 | destination = Path(directory) / 'helloworld.txt' 202 | 203 | source.copy(destination) 204 | 205 | self.assertTrue(destination.is_file()) 206 | 207 | with open(str(destination), 'r') as file: 208 | text = file.read() 209 | 210 | self.assertEqual(text, source.read_text()) 211 | 212 | def test_copy_binary(self): 213 | with tempfile.TemporaryDirectory() as directory: 214 | source = ResourcePath("Packages/test_package/UTF-8-test.txt") 215 | destination = Path(directory) / 'UTF-8-test.txt' 216 | 217 | source.copy(destination) 218 | 219 | self.assertTrue(destination.is_file()) 220 | 221 | with open(str(destination), 'rb') as file: 222 | data = file.read() 223 | 224 | self.assertEqual(data, source.read_bytes()) 225 | 226 | def test_copy_existing(self): 227 | with tempfile.TemporaryDirectory() as directory: 228 | source = ResourcePath("Packages/test_package/helloworld.txt") 229 | destination = Path(directory) / 'helloworld.txt' 230 | 231 | with open(str(destination), 'w') as file: 232 | file.write("Nothing to see here.\n") 233 | 234 | source.copy(destination) 235 | 236 | self.assertTrue(destination.is_file()) 237 | 238 | with open(str(destination), 'r') as file: 239 | text = file.read() 240 | 241 | self.assertEqual(text, source.read_text()) 242 | 243 | def test_copy_existing_error(self): 244 | with tempfile.TemporaryDirectory() as directory: 245 | source = ResourcePath("Packages/test_package/helloworld.txt") 246 | destination = Path(directory) / 'helloworld.txt' 247 | 248 | text = "Nothing to see here.\n" 249 | with open(str(destination), 'w') as file: 250 | file.write(text) 251 | 252 | with self.assertRaises(FileExistsError): 253 | source.copy(destination, False) 254 | 255 | def test_copy_directory_error(self): 256 | with tempfile.TemporaryDirectory() as directory: 257 | source = ResourcePath("Packages/test_package/helloworld.txt") 258 | destination = Path(directory) / 'helloworld.txt' 259 | 260 | destination.mkdir() 261 | 262 | with self.assertRaises(IsADirectoryError): 263 | source.copy(destination) 264 | 265 | self.assertTrue(destination.is_dir()) 266 | 267 | def test_copytree(self): 268 | with tempfile.TemporaryDirectory() as directory: 269 | source = ResourcePath("Packages/test_package") 270 | destination = Path(directory) / 'tree' 271 | 272 | source.copytree(destination) 273 | 274 | self.assertEqual( 275 | { 276 | path.relative_to(destination).parts 277 | for path in destination.rglob('*') 278 | if path.is_file() 279 | }, 280 | { 281 | path.relative_to(source) 282 | for path in source.rglob('*') 283 | } 284 | ) 285 | 286 | def test_copytree_exists_error(self): 287 | with tempfile.TemporaryDirectory() as directory: 288 | source = ResourcePath("Packages/test_package") 289 | destination = Path(directory) / 'tree' 290 | destination.mkdir() 291 | 292 | with self.assertRaises(FileExistsError): 293 | source.copytree(destination) 294 | 295 | def test_copytree_exists(self): 296 | with tempfile.TemporaryDirectory() as directory: 297 | source = ResourcePath("Packages/test_package") 298 | destination = Path(directory) / 'tree' 299 | destination.mkdir() 300 | 301 | helloworld_file = destination / 'helloworld.txt' 302 | 303 | with open(str(helloworld_file), 'w') as file: 304 | file.write("Nothing to see here.\n") 305 | 306 | source.copytree(destination, exist_ok=True) 307 | 308 | self.assertEqual( 309 | { 310 | path.relative_to(destination).parts 311 | for path in destination.rglob('*') 312 | if path.is_file() 313 | }, 314 | { 315 | path.relative_to(source) 316 | for path in source.rglob('*') 317 | } 318 | ) 319 | 320 | with open(str(helloworld_file)) as file: 321 | helloworld_contents = file.read() 322 | 323 | self.assertEqual( 324 | helloworld_contents, 325 | (source / 'helloworld.txt').read_text() 326 | ) 327 | -------------------------------------------------------------------------------- /docs/source/extensions/better_toctree.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | sphinx.environment.collectors.toctree 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | Toctree collector for sphinx.environment. 7 | 8 | :copyright: Copyright 2007-2018 by the Sphinx team, see AUTHORS. 9 | :license: BSD, see LICENSE for details. 10 | """ 11 | 12 | from typing import cast 13 | 14 | from docutils import nodes 15 | 16 | from sphinx import addnodes 17 | from sphinx.environment.adapters.toctree import TocTree 18 | from sphinx.environment.collectors import EnvironmentCollector 19 | from sphinx.locale import __ 20 | from sphinx.transforms import SphinxContentsFilter 21 | from sphinx.util import url_re, logging 22 | 23 | if False: 24 | # For type annotation 25 | from typing import Any, Dict, List, Set, Tuple, Type, TypeVar # NOQA 26 | from sphinx.application import Sphinx # NOQA 27 | from sphinx.builders import Builder # NOQA 28 | from sphinx.environment import BuildEnvironment # NOQA 29 | from sphinx.util.typing import unicode # NOQA 30 | 31 | N = TypeVar('N') 32 | 33 | logger = logging.getLogger(__name__) 34 | 35 | 36 | class TocTreeCollector(EnvironmentCollector): 37 | def clear_doc(self, app, env, docname): 38 | # type: (Sphinx, BuildEnvironment, unicode) -> None 39 | env.tocs.pop(docname, None) 40 | env.toc_secnumbers.pop(docname, None) 41 | env.toc_fignumbers.pop(docname, None) 42 | env.toc_num_entries.pop(docname, None) 43 | env.toctree_includes.pop(docname, None) 44 | env.glob_toctrees.discard(docname) 45 | env.numbered_toctrees.discard(docname) 46 | 47 | for subfn, fnset in list(env.files_to_rebuild.items()): 48 | fnset.discard(docname) 49 | if not fnset: 50 | del env.files_to_rebuild[subfn] 51 | 52 | def merge_other(self, app, env, docnames, other): 53 | # type: (Sphinx, BuildEnvironment, Set[unicode], BuildEnvironment) -> None 54 | for docname in docnames: 55 | env.tocs[docname] = other.tocs[docname] 56 | env.toc_num_entries[docname] = other.toc_num_entries[docname] 57 | if docname in other.toctree_includes: 58 | env.toctree_includes[docname] = other.toctree_includes[docname] 59 | if docname in other.glob_toctrees: 60 | env.glob_toctrees.add(docname) 61 | if docname in other.numbered_toctrees: 62 | env.numbered_toctrees.add(docname) 63 | 64 | for subfn, fnset in other.files_to_rebuild.items(): 65 | env.files_to_rebuild.setdefault(subfn, set()).update(fnset & set(docnames)) 66 | 67 | def process_doc(self, app, doctree): 68 | # type: (Sphinx, nodes.document) -> None 69 | """Build a TOC from the doctree and store it in the inventory.""" 70 | docname = app.env.docname 71 | numentries = [0] # nonlocal again... 72 | 73 | def traverse_in_section(node, cls): 74 | # type: (nodes.Element, Type[N]) -> List[N] 75 | """Like traverse(), but stay within the same section.""" 76 | result = [] # type: List[N] 77 | if isinstance(node, cls): 78 | result.append(node) 79 | for child in node.children: 80 | if isinstance(child, nodes.section): 81 | continue 82 | elif isinstance(child, nodes.Element): 83 | result.extend(traverse_in_section(child, cls)) 84 | return result 85 | 86 | def build_toc(node, depth=1): 87 | # type: (nodes.Element, int) -> nodes.bullet_list 88 | entries = [] # type: List[nodes.Element] 89 | for sectionnode in node: 90 | # find all toctree nodes in this section and add them 91 | # to the toc (just copying the toctree node which is then 92 | # resolved in self.get_and_resolve_doctree) 93 | if isinstance(sectionnode, nodes.section): 94 | title = sectionnode[0] 95 | # copy the contents of the section title, but without references 96 | # and unnecessary stuff 97 | visitor = SphinxContentsFilter(doctree) 98 | title.walkabout(visitor) 99 | nodetext = visitor.get_entry_text() 100 | if not numentries[0]: 101 | # for the very first toc entry, don't add an anchor 102 | # as it is the file's title anyway 103 | anchorname = '' 104 | else: 105 | anchorname = '#' + sectionnode['ids'][0] 106 | numentries[0] += 1 107 | # make these nodes: 108 | # list_item -> compact_paragraph -> reference 109 | reference = nodes.reference( 110 | '', '', internal=True, refuri=docname, 111 | anchorname=anchorname, *nodetext) 112 | para = addnodes.compact_paragraph('', '', reference) 113 | item = nodes.list_item('', para) # type: nodes.Element 114 | sub_item = build_toc(sectionnode, depth + 1) 115 | if sub_item: 116 | item += sub_item 117 | entries.append(item) 118 | elif isinstance(sectionnode, addnodes.only): 119 | onlynode = addnodes.only(expr=sectionnode['expr']) 120 | blist = build_toc(sectionnode, depth) 121 | if blist: 122 | onlynode += blist.children 123 | entries.append(onlynode) 124 | # ADDED 125 | elif isinstance(sectionnode, addnodes.desc): 126 | target = next(iter(sectionnode.traverse(addnodes.desc_signature))) 127 | 128 | reference = nodes.reference( 129 | '', '', 130 | nodes.literal('', target.attributes['fullname']), 131 | internal=True, 132 | refuri=docname, 133 | anchorname='#' + target.attributes['ids'][0], 134 | ) 135 | 136 | entries.append( 137 | nodes.list_item( 138 | '', 139 | addnodes.compact_paragraph('', '', reference) 140 | ) 141 | ) 142 | # /ADDED 143 | elif isinstance(sectionnode, nodes.Element): 144 | for toctreenode in traverse_in_section(sectionnode, 145 | addnodes.toctree): 146 | item = toctreenode.copy() 147 | entries.append(item) 148 | # important: do the inventory stuff 149 | TocTree(app.env).note(docname, toctreenode) 150 | if entries: 151 | return nodes.bullet_list('', *entries) 152 | return None 153 | toc = build_toc(doctree) 154 | if toc: 155 | app.env.tocs[docname] = toc 156 | else: 157 | app.env.tocs[docname] = nodes.bullet_list('') 158 | app.env.toc_num_entries[docname] = numentries[0] 159 | 160 | def get_updated_docs(self, app, env): 161 | # type: (Sphinx, BuildEnvironment) -> List[unicode] 162 | return self.assign_section_numbers(env) + self.assign_figure_numbers(env) 163 | 164 | def assign_section_numbers(self, env): 165 | # type: (BuildEnvironment) -> List[unicode] 166 | """Assign a section number to each heading under a numbered toctree.""" 167 | # a list of all docnames whose section numbers changed 168 | rewrite_needed = [] 169 | 170 | assigned = set() # type: Set[unicode] 171 | old_secnumbers = env.toc_secnumbers 172 | env.toc_secnumbers = {} 173 | 174 | def _walk_toc(node, secnums, depth, titlenode=None): 175 | # type: (nodes.Element, Dict, int, nodes.title) -> None 176 | # titlenode is the title of the document, it will get assigned a 177 | # secnumber too, so that it shows up in next/prev/parent rellinks 178 | for subnode in node.children: 179 | if isinstance(subnode, nodes.bullet_list): 180 | numstack.append(0) 181 | _walk_toc(subnode, secnums, depth - 1, titlenode) 182 | numstack.pop() 183 | titlenode = None 184 | elif isinstance(subnode, nodes.list_item): 185 | _walk_toc(subnode, secnums, depth, titlenode) 186 | titlenode = None 187 | elif isinstance(subnode, addnodes.only): 188 | # at this stage we don't know yet which sections are going 189 | # to be included; just include all of them, even if it leads 190 | # to gaps in the numbering 191 | _walk_toc(subnode, secnums, depth, titlenode) 192 | titlenode = None 193 | elif isinstance(subnode, addnodes.compact_paragraph): 194 | numstack[-1] += 1 195 | reference = cast(nodes.reference, subnode[0]) 196 | if depth > 0: 197 | number = list(numstack) 198 | secnums[reference['anchorname']] = tuple(numstack) 199 | else: 200 | number = None 201 | secnums[reference['anchorname']] = None 202 | reference['secnumber'] = number 203 | if titlenode: 204 | titlenode['secnumber'] = number 205 | titlenode = None 206 | elif isinstance(subnode, addnodes.toctree): 207 | _walk_toctree(subnode, depth) 208 | 209 | def _walk_toctree(toctreenode, depth): 210 | # type: (addnodes.toctree, int) -> None 211 | if depth == 0: 212 | return 213 | for (title, ref) in toctreenode['entries']: 214 | if url_re.match(ref) or ref == 'self': 215 | # don't mess with those 216 | continue 217 | elif ref in assigned: 218 | logger.warning(__('%s is already assigned section numbers ' 219 | '(nested numbered toctree?)'), ref, 220 | location=toctreenode, type='toc', subtype='secnum') 221 | elif ref in env.tocs: 222 | secnums = {} # type: Dict[unicode, Tuple[int, ...]] 223 | env.toc_secnumbers[ref] = secnums 224 | assigned.add(ref) 225 | _walk_toc(env.tocs[ref], secnums, depth, env.titles.get(ref)) 226 | if secnums != old_secnumbers.get(ref): 227 | rewrite_needed.append(ref) 228 | 229 | for docname in env.numbered_toctrees: 230 | assigned.add(docname) 231 | doctree = env.get_doctree(docname) 232 | for toctreenode in doctree.traverse(addnodes.toctree): 233 | depth = toctreenode.get('numbered', 0) 234 | if depth: 235 | # every numbered toctree gets new numbering 236 | numstack = [0] 237 | _walk_toctree(toctreenode, depth) 238 | 239 | return rewrite_needed 240 | 241 | def assign_figure_numbers(self, env): 242 | # type: (BuildEnvironment) -> List[unicode] 243 | """Assign a figure number to each figure under a numbered toctree.""" 244 | 245 | rewrite_needed = [] 246 | 247 | assigned = set() # type: Set[unicode] 248 | old_fignumbers = env.toc_fignumbers 249 | env.toc_fignumbers = {} 250 | fignum_counter = {} # type: Dict[unicode, Dict[Tuple[int, ...], int]] 251 | 252 | def get_figtype(node): 253 | # type: (nodes.Node) -> unicode 254 | for domain in env.domains.values(): 255 | figtype = domain.get_enumerable_node_type(node) 256 | if figtype: 257 | return figtype 258 | 259 | return None 260 | 261 | def get_section_number(docname, section): 262 | # type: (unicode, nodes.section) -> Tuple[int, ...] 263 | anchorname = '#' + section['ids'][0] 264 | secnumbers = env.toc_secnumbers.get(docname, {}) 265 | if anchorname in secnumbers: 266 | secnum = secnumbers.get(anchorname) 267 | else: 268 | secnum = secnumbers.get('') 269 | 270 | return secnum or tuple() 271 | 272 | def get_next_fignumber(figtype, secnum): 273 | # type: (unicode, Tuple[int, ...]) -> Tuple[int, ...] 274 | counter = fignum_counter.setdefault(figtype, {}) 275 | 276 | secnum = secnum[:env.config.numfig_secnum_depth] 277 | counter[secnum] = counter.get(secnum, 0) + 1 278 | return secnum + (counter[secnum],) 279 | 280 | def register_fignumber(docname, secnum, figtype, fignode): 281 | # type: (unicode, Tuple[int, ...], unicode, nodes.Element) -> None 282 | env.toc_fignumbers.setdefault(docname, {}) 283 | fignumbers = env.toc_fignumbers[docname].setdefault(figtype, {}) 284 | figure_id = fignode['ids'][0] 285 | 286 | fignumbers[figure_id] = get_next_fignumber(figtype, secnum) 287 | 288 | def _walk_doctree(docname, doctree, secnum): 289 | # type: (unicode, nodes.Element, Tuple[int, ...]) -> None 290 | for subnode in doctree.children: 291 | if isinstance(subnode, nodes.section): 292 | next_secnum = get_section_number(docname, subnode) 293 | if next_secnum: 294 | _walk_doctree(docname, subnode, next_secnum) 295 | else: 296 | _walk_doctree(docname, subnode, secnum) 297 | elif isinstance(subnode, addnodes.toctree): 298 | for title, subdocname in subnode['entries']: 299 | if url_re.match(subdocname) or subdocname == 'self': 300 | # don't mess with those 301 | continue 302 | 303 | _walk_doc(subdocname, secnum) 304 | elif isinstance(subnode, nodes.Element): 305 | figtype = get_figtype(subnode) 306 | if figtype and subnode['ids']: 307 | register_fignumber(docname, secnum, figtype, subnode) 308 | 309 | _walk_doctree(docname, subnode, secnum) 310 | 311 | def _walk_doc(docname, secnum): 312 | # type: (unicode, Tuple[int, ...]) -> None 313 | if docname not in assigned: 314 | assigned.add(docname) 315 | doctree = env.get_doctree(docname) 316 | _walk_doctree(docname, doctree, secnum) 317 | 318 | if env.config.numfig: 319 | _walk_doc(env.config.master_doc, tuple()) 320 | for docname, fignums in env.toc_fignumbers.items(): 321 | if fignums != old_fignumbers.get(docname): 322 | rewrite_needed.append(docname) 323 | 324 | return rewrite_needed 325 | --------------------------------------------------------------------------------