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