├── .editorconfig ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .python-version ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-commands ├── Main.sublime-menu ├── README.md ├── __init__.py ├── lib ├── __init__.py ├── entities.py └── settings.py ├── license.txt ├── mypy.ini ├── tabfilter.py ├── tabfilter.sublime-settings ├── tests ├── fixtures │ ├── bar.txt │ └── foo.txt ├── test_entities.py ├── test_settings.py └── test_tabfilter.py └── unittesting.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | 9 | [.py] 10 | charset = utf-8 11 | indent_size = 4 12 | indent_style = space 13 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run-tests: 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | st-version: [4] 11 | os: ["ubuntu-latest", "macOS-latest", "windows-latest"] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: SublimeText/UnitTesting/actions/setup@v1 16 | with: 17 | sublime-text-version: ${{ matrix.st-version }} 18 | package-name: "Tab Filter" 19 | - uses: SublimeText/UnitTesting/actions/run-tests@v1 20 | with: 21 | package-name: "Tab Filter" 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | .mypy_cache 4 | .vscode/ -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["shift+alt+p"], 4 | "command": "tab_filter" 5 | }, 6 | { 7 | "keys": ["shift+alt+a"], 8 | "command": "tab_filter", 9 | "args": { 10 | "active_group_only": true, 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["shift+alt+p"], 4 | "command": "tab_filter" 5 | }, 6 | { 7 | "keys": ["shift+alt+a"], 8 | "command": "tab_filter", 9 | "args": { 10 | "active_group_only": true, 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["shift+alt+p"], 4 | "command": "tab_filter" 5 | }, 6 | { 7 | "keys": ["shift+alt+a"], 8 | "command": "tab_filter", 9 | "args": { 10 | "active_group_only": true, 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Tab Filter", 4 | "command": "tab_filter" 5 | }, 6 | { 7 | "caption": "Tab Filter (Active Group)", 8 | "command": "tab_filter", 9 | "args": { 10 | "active_group_only": true, 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption" : "Preferences", 4 | "id" : "preferences", 5 | "mnemonic": "n", 6 | "children" : 7 | [ 8 | { 9 | "caption" : "Package Settings", 10 | "id" : "package-settings", 11 | "mnemonic": "P", 12 | "children" : 13 | [ 14 | { 15 | "caption" : "Tab Filter", 16 | "children" : 17 | [ 18 | { 19 | "command" : "open_file", 20 | "args" : { "file" : "${packages}/Tab Filter/tabfilter.sublime-settings"}, 21 | "caption" : "Settings - Default" 22 | }, 23 | { 24 | "command" : "open_file", 25 | "args" : { "file" : "${packages}/User/tabfilter.sublime-settings"}, 26 | "caption" : "Settings - User" 27 | }, 28 | { 29 | "caption": "-" 30 | }, 31 | { 32 | "command": "open_file", 33 | "args": 34 | { 35 | "file": "${packages}/Tab Filter/Default (OSX).sublime-keymap", 36 | "platform": "OSX" 37 | }, 38 | "caption": "Key Bindings – Default" 39 | }, 40 | { 41 | "command": "open_file", 42 | "args": 43 | { 44 | "file": "${packages}/Tab Filter/Default (Linux).sublime-keymap", 45 | "platform": "Linux" 46 | }, 47 | "caption": "Key Bindings – Default" 48 | }, 49 | { 50 | "command": "open_file", 51 | "args": 52 | { 53 | "file": "${packages}/Tab Filter/Default (Windows).sublime-keymap", 54 | "platform": "Windows" 55 | }, 56 | "caption": "Key Bindings – Default" 57 | }, 58 | { 59 | "command": "open_file", 60 | "args": 61 | { 62 | "file": "${packages}/User/Default (OSX).sublime-keymap", 63 | "platform": "OSX" 64 | }, 65 | "caption": "Key Bindings – User" 66 | }, 67 | { 68 | "command": "open_file", 69 | "args": 70 | { 71 | "file": "${packages}/User/Default (Linux).sublime-keymap", 72 | "platform": "Linux" 73 | }, 74 | "caption": "Key Bindings – User" 75 | }, 76 | { 77 | "command": "open_file", 78 | "args": 79 | { 80 | "file": "${packages}/User/Default (Windows).sublime-keymap", 81 | "platform": "Windows" 82 | }, 83 | "caption": "Key Bindings – User" 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | } 91 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tab Filter 2 | ![Tests](https://github.com/robinmalburn/sublime-tabfilter/actions/workflows/tests.yml/badge.svg?branch=master&event=push) 3 | 4 | 5 | Tab Filter is a Sublime Text plugin for quickly switching between open tabs. Invoking Tab Filter brings up a "GoTo Anything"-like quick input showing your opened tabs for the current window, allowing you to quick filter on file names to rapidly switch amongst existing tabs. 6 | 7 | ## Compatibility 8 | 9 | This plugin is compatible with Sublime Text 4. 10 | 11 | For Sublime Text 2 and 3 support, please see [release 1.4](https://github.com/robinmalburn/sublime-tabfilter/tree/release/1.4) - still also avalable via Package Control, though no longer actively updated with new features. 12 | 13 | ## Installation 14 | 15 | ### Package Control 16 | 17 | Tab Filter is also available through [Package Control](http://wbond.net/sublime\_packages/package\_control). To install, bring up the Command Palette (brought up using `ctrl+shift+p` on Linux / Windows or `cmd+shift+p` on OS X) and run the `Package Control: Install Package` command - now search for and select **Tab Filter**. 18 | 19 | ### Manual 20 | 21 | From within Sublime Text, go to the `Preferences` > `Browse Pacakges` menu; this should open up your file browser at the correct location for where your copy of Sublime text stores all packages. 22 | 23 | From within this folder you can install... 24 | 25 | #### Using git 26 | 27 | You can install within the Packages folder opened by running the following from a terminal / console: 28 | 29 | $ git clone git://github.com/robinmalburn/sublime-tabfilter.git 'Tab Filter' 30 | 31 | #### Without git 32 | 33 | To install without git, download the source code as a zip file and extract the contents to into a subfolder of the Packages folder called `Tab Filter` 34 | 35 | ## Usage 36 | 37 | ### Key Bindings 38 | 39 | Tab Filter comes with two built in commands: 40 | 41 | **Note:** All keybindings can be overriden via the keybindings options in `Preferences > Package Settings > Tab Filter > Key Bindings - User` 42 | 43 | #### Standard 44 | This command searches and filters across all open windows and groups, and has a default keymap on Linux, OSX and Windows of: `alt+shift+p` 45 | 46 | #### Active Group 47 | This command searches and filters just within the active group when using a split layout in Sublime Text. The default keymap for this command on Linux, OSX and Windows is: `alt+shift+a` 48 | 49 | 50 | ### Command Palette 51 | 52 | Tab Filter can also be activated via the Command Palette (brought up using `ctrl+shift+p` on Linux / Windows or `cmd+shift+p` on OS X) and typing Tab Filter 53 | 54 | ### Settings 55 | 56 | Additional configuration settings for Tab Filter can be altered via `Preferences > Package Settings > Tab Filter > Settings - User` 57 | 58 | ##### Captions 59 | 60 | Tab Filter can be configured to show or hide additional captions relating to the state of each open tab. The captions include: *Current File*, *Unsaved File*, *Unsaved Changes* and *Read Only*. Captions are shown by default, but this behaviour can be changed by setting the `show_captions` setting to `false`. 61 | 62 | ##### Path/Filename Filtering 63 | 64 | By default, Tab Filter only shows the basename of open tabs (where they're really files and not just buffers, of course). This configuration can be changed to instead show and therefore allow filtering by the full, non-common path of the file instead by changing the `include_path` option to `true`. 65 | 66 | ##### Preview Currently Selected Entry 67 | 68 | By default, Tab Filter only focuses the tab if it gets selected. To always focus/preview the currently highlighted entry, set `preview_tab` to `true`. **Note** that this currently only works with a single group layout (no split window). 69 | 70 | ##### Group Caption 71 | 72 | By default, Tab Filter only shows information about the filename being worked on, and optionally some meta information available via Path and Captions mentioned above. With this feature, for users who make heavy use of multi-pane layouts, you can now include an indicator of what group a tab belongs to, meaning you can easily see which pane a tab belongs in and differentiate between similarly named files open across different panes. Set `show_group_caption` to `true` to enable the feature. **Note:** Even if enabled, the feature will only display if there is more than one group, otherwise it's a fairly pointless caption to distract from the relevant information, since every group would be `Group: 1` anyhow. 73 | 74 | ## License 75 | 76 | Released under [MIT license](https://github.com/robinmalburn/sublime-tabfilter/blob/master/license.txt). 77 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinmalburn/sublime-tabfilter/b170640d4176eea69227218e814a87311e94a059/__init__.py -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robinmalburn/sublime-tabfilter/b170640d4176eea69227218e814a87311e94a059/lib/__init__.py -------------------------------------------------------------------------------- /lib/entities.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | from os import path 5 | from sublime import View # type: ignore 6 | from typing import Optional, List 7 | 8 | 9 | class Tab(object): 10 | """Represent a Sublime tab and the relevant metadata.""" 11 | view: View 12 | title: str = "untitled" 13 | subtitle: str = "untitled" 14 | is_file: bool = True 15 | path: Optional[str] = "" 16 | captions: List[str] = [] 17 | 18 | def __init__(self, view: View) -> None: 19 | """Initialise the Tab.""" 20 | self.view = view 21 | self.captions = [] 22 | 23 | name: Optional[str] = view.file_name() 24 | 25 | if name is None: 26 | # If the name is not set, then we're dealing with a buffer 27 | # rather than a file, so deal with it accordingly. 28 | self.is_file = False 29 | name = view.name() 30 | 31 | # set the view name to untitled if we get an empty name 32 | if len(name) == 0: 33 | name = "untitled" 34 | 35 | self.set_title(name) 36 | self.set_subtitle(name) 37 | 38 | if self.is_file is True: 39 | self.path = path.dirname(name) 40 | self.set_title(path.basename(name)) 41 | 42 | def get_title(self) -> str: 43 | """Gets the title of the tab.""" 44 | return self.title 45 | 46 | def set_title(self, title: str) -> None: 47 | """Sets the title of the tab.""" 48 | self.title = title 49 | 50 | def get_subtitle(self) -> str: 51 | """Get the subtitle of the tab.""" 52 | return self.subtitle 53 | 54 | def set_subtitle(self, subtitle: str) -> None: 55 | """Set the subtitle of the tab.""" 56 | self.subtitle = subtitle 57 | 58 | def is_file_view(self) -> bool: 59 | """Gets whether the tab's view is a file or not.""" 60 | return self.is_file 61 | 62 | def get_path(self) -> str: 63 | """Gets the path for a view tab, or None otherwise.""" 64 | return self.path 65 | 66 | def get_view(self) -> View: 67 | """Gets the view associated with the tab.""" 68 | return self.view 69 | 70 | def add_caption(self, caption: str) -> None: 71 | """Adds the caption to the list of captions for this Tab.""" 72 | self.captions.append(str(caption)) 73 | 74 | def get_captions(self) -> List[str]: 75 | """Gets the current captions.""" 76 | return self.captions[:] 77 | 78 | def get_details(self) -> List[str]: 79 | """Returns a list of tab details.""" 80 | details: List[str] = [self.get_title(), self.get_subtitle()] 81 | 82 | captions: List[str] = self.get_captions() 83 | 84 | if len(captions) > 0: 85 | details.append(", ".join(captions)) 86 | 87 | return details 88 | 89 | def __eq__(self, obj) -> bool: 90 | """Ensures two tabs refer to the same underlying view and data.""" 91 | if isinstance(obj, type(self)) is False: 92 | return False 93 | return ( 94 | self.get_view() == obj.get_view() 95 | and self.get_captions() == obj.get_captions() 96 | and self.get_title() == obj.get_title() 97 | and self.get_subtitle() == obj.get_subtitle() 98 | ) 99 | 100 | def __str__(self) -> str: 101 | return self.get_title() -------------------------------------------------------------------------------- /lib/settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | from abc import ABC, abstractmethod 5 | from typing import List, Dict, Union 6 | from .entities import Tab 7 | from sublime import Settings, View, Window # type: ignore 8 | from os import path 9 | 10 | DEFAULT_SETINGS: Dict[str, Union[bool, str]] = { 11 | "show_captions": True, 12 | "include_path": False, 13 | "preview_tab": False, 14 | "show_group_caption": False, 15 | } 16 | 17 | 18 | class Setting(ABC): 19 | """A single setting relating to the package.""" 20 | 21 | @abstractmethod 22 | def is_enabled(self) -> bool: 23 | """Returns if the setting is enabled or not.""" 24 | 25 | 26 | class TabSetting(Setting): 27 | """A setting relating to one or more tabs.""" 28 | 29 | settings: Settings 30 | window: Window 31 | 32 | def __init__(self, settings: Settings, window: Window) -> None: 33 | """Initialise the setting instance with a copy 34 | of the sublime package settings. 35 | """ 36 | self.settings = settings 37 | self.window = window 38 | 39 | @abstractmethod 40 | def apply(self, tabs: List[Tab]) -> List[Tab]: 41 | """Applies the setting to the given list of tabs.""" 42 | 43 | 44 | class ShowCaptionsTabSetting(TabSetting): 45 | """Setting for showing captions on tabs.""" 46 | def is_enabled(self) -> bool: 47 | return self.settings.get("show_captions") is True 48 | 49 | def apply(self, tabs: List[Tab]) -> List[Tab]: 50 | if self.is_enabled() is False: 51 | return tabs 52 | 53 | for tab in tabs: 54 | self._populate_captions(tab) 55 | return tabs 56 | 57 | def _populate_captions(self, tab: Tab) -> None: 58 | view: View = tab.get_view() 59 | if view.window().active_view().id() == view.id(): 60 | tab.add_caption("Current File") 61 | 62 | if view.file_name() is None: 63 | tab.add_caption("Unsaved File") 64 | elif view.is_dirty(): 65 | tab.add_caption("Unsaved Changes") 66 | 67 | if view.is_read_only(): 68 | tab.add_caption("Read Only") 69 | 70 | 71 | class IncludePathTabSetting(TabSetting): 72 | """Setting for including the path on tabs.""" 73 | def is_enabled(self) -> bool: 74 | return self.settings.get("include_path") is True 75 | 76 | def apply(self, tabs: List[Tab]) -> List[Tab]: 77 | if self.is_enabled() is False: 78 | return tabs 79 | 80 | for tab in tabs: 81 | if tab.is_file_view() is True: 82 | tab.set_title(tab.get_subtitle()) 83 | return tabs 84 | 85 | 86 | class ShowGroupCaptionTabSetting(TabSetting): 87 | """Setting for showing captions on tabs.""" 88 | def is_enabled(self) -> bool: 89 | return ( 90 | self.settings.get("show_group_caption") is True 91 | and self.window.num_groups() > 1 92 | ) 93 | 94 | def apply(self, tabs: List[Tab]) -> List[Tab]: 95 | if self.is_enabled() is False: 96 | return tabs 97 | 98 | for tab in tabs: 99 | view: View = tab.get_view() 100 | # Group's are zero based, so lets add 1 one to the offset 101 | # to make them a bit more human friendly. 102 | group: int = view.sheet().group() + 1 103 | tab.add_caption(f"Group: {group}") 104 | return tabs 105 | 106 | 107 | class CommonPrefixTabSetting(TabSetting): 108 | """Setting for truncating the common prefix on files.""" 109 | def is_enabled(self) -> bool: 110 | # There's currently no support for opting out of this "setting". 111 | return True 112 | 113 | def apply(self, tabs: List[Tab]) -> List[Tab]: 114 | if self.is_enabled() is False: 115 | return tabs 116 | 117 | prefix: int = 0 118 | common_prefix: str = path.commonprefix( 119 | [ 120 | tab.get_path() 121 | for tab in tabs 122 | if tab.is_file_view() 123 | ] 124 | ) 125 | 126 | if path.isdir(common_prefix) is False: 127 | common_prefix = common_prefix[:common_prefix.rfind(path.sep)] 128 | prefix = len(common_prefix) 129 | 130 | if prefix > 0: 131 | for tab in tabs: 132 | if tab.is_file_view(): 133 | tab.set_subtitle(f"...{tab.get_subtitle()[prefix:]}") 134 | return tabs 135 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2021 Robin Malburn 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 11 | all 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 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.8 3 | show_error_codes = True 4 | 5 | [mypy-*] 6 | check_untyped_defs = True 7 | disallow_untyped_calls = True 8 | disallow_untyped_defs = True 9 | ignore_missing_imports = True 10 | no_implicit_optional = True 11 | strict_optional = True 12 | warn_return_any = True 13 | warn_unused_configs = True 14 | warn_redundant_casts = True 15 | warn_unused_ignores = True 16 | -------------------------------------------------------------------------------- /tabfilter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | import sublime # type: ignore 5 | import sublime_plugin # type: ignore 6 | from typing import List, Tuple 7 | from .lib.entities import Tab 8 | from .lib.settings import ( 9 | TabSetting, 10 | CommonPrefixTabSetting, 11 | ShowCaptionsTabSetting, 12 | IncludePathTabSetting, 13 | ShowGroupCaptionTabSetting, 14 | ) 15 | 16 | 17 | class TabFilterCommand(sublime_plugin.WindowCommand): 18 | """Provides a GoToAnything style interface for 19 | searching and selecting open tabs. 20 | """ 21 | window: sublime.Window 22 | views: List[sublime.View] = [] 23 | current_tab_idx: int = -1 24 | settings: sublime.Settings 25 | 26 | def gather_tabs(self, group_indexes: List[int]) -> List[Tab]: 27 | """Gather tabs from the given group indexes.""" 28 | tabs: List[Tab] = [] 29 | idx: int = 0 30 | self.views = [] 31 | for group_idx in group_indexes: 32 | for view in self.window.views_in_group(group_idx): 33 | self.views.append(view) 34 | if self.window.active_view().id() == view.id(): 35 | # save index for later usage 36 | self.current_tab_idx = idx 37 | tabs.append(Tab(view)) 38 | idx = idx + 1 39 | return tabs 40 | 41 | def format_tabs( 42 | self, 43 | tabs: List[Tab], 44 | formatting_settings: Tuple[TabSetting, ...] 45 | ) -> List[List[str]]: 46 | """Formats tabs for display in the quick info panel.""" 47 | for setting in formatting_settings: 48 | tabs = setting.apply(tabs) 49 | 50 | return [tab.get_details() for tab in tabs] 51 | 52 | def display_quick_info_panel( 53 | self, 54 | tabs: List[List[str]], 55 | preview: bool 56 | ) -> None: 57 | """Displays the quick info panel with the formatted tabs.""" 58 | if preview is True: 59 | self.window.show_quick_panel( 60 | tabs, 61 | self.on_done, 62 | on_highlight=self.on_highlighted, 63 | selected_index=self.current_tab_idx 64 | ) 65 | return 66 | 67 | self.window.show_quick_panel(tabs, self.on_done) 68 | 69 | def on_done(self, index: int) -> None: 70 | """Callback handler to move focus to the selected tab index.""" 71 | if index == -1 and self.current_tab_idx != -1: 72 | # If the selection was quit, re-focus the last selected Tab 73 | self.window.focus_view(self.views[self.current_tab_idx]) 74 | elif index > -1 and index < len(self.views): 75 | self.window.focus_view(self.views[index]) 76 | 77 | def on_highlighted(self, index: int) -> None: 78 | """Callback handler to focus the currently highlighted Tab.""" 79 | if index > -1 and index < len(self.views): 80 | self.window.focus_view(self.views[index]) 81 | 82 | def run(self, active_group_only=False) -> None: 83 | """Shows a quick panel to filter and select tabs from 84 | the active window. 85 | """ 86 | self.views = [] 87 | self.settings = sublime.load_settings("tabfilter.sublime-settings") 88 | 89 | groups: List[int] = [self.window.active_group()] 90 | 91 | if active_group_only is False: 92 | groups = list(range(self.window.num_groups())) 93 | 94 | tabs = self.gather_tabs(groups) 95 | 96 | preview: bool = self.settings.get("preview_tab") is True 97 | 98 | if active_group_only is False: 99 | preview = preview and self.window.num_groups() == 1 100 | 101 | formatting_settings: Tuple[TabSetting, ...] = ( 102 | CommonPrefixTabSetting(self.settings, self.window), 103 | ShowGroupCaptionTabSetting(self.settings, self.window), 104 | ShowCaptionsTabSetting(self.settings, self.window), 105 | IncludePathTabSetting(self.settings, self.window), 106 | ) 107 | 108 | self.display_quick_info_panel( 109 | self.format_tabs(tabs, formatting_settings), 110 | preview 111 | ) 112 | -------------------------------------------------------------------------------- /tabfilter.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Show additonal captions, e.g. "Current File", "Unsaved Changes", etc 4 | * @param boolean 5 | */ 6 | "show_captions" : true, 7 | /** 8 | * Include path along with filename when displaying quick panel. 9 | * @param boolean 10 | */ 11 | "include_path" : false, 12 | /** 13 | * Allows focus/preview of the currently highlighted entry. Note that this currently only works with a single group layout. 14 | * @param boolean 15 | */ 16 | "preview_tab" : false, 17 | /** 18 | * Display a caption for the group a tab belongs to. 19 | * @param boolean 20 | */ 21 | "show_group_caption": false, 22 | /** 23 | * The group caption to show before the group index when show_group_caption is enabled. 24 | * @param string 25 | */ 26 | "group_caption": "Group:", 27 | } -------------------------------------------------------------------------------- /tests/fixtures/bar.txt: -------------------------------------------------------------------------------- 1 | Bar fixture for testing a file on disk. -------------------------------------------------------------------------------- /tests/fixtures/foo.txt: -------------------------------------------------------------------------------- 1 | Foo fixture for testing a file on disk. -------------------------------------------------------------------------------- /tests/test_entities.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | import sublime # type: ignore 5 | from os import path 6 | from unittest import TestCase 7 | from typing import List, Tuple, Optional 8 | try: 9 | from lib import entities 10 | except ImportError: 11 | # If we're running these tests in UnitTesting, then we need to use 12 | # The package name - Tab Filter - so let's grab import lib and try again. 13 | from importlib import import_module 14 | entities = import_module(".lib.entities", "Tab Filter") 15 | 16 | Tab = entities.Tab 17 | 18 | 19 | class TabTestCase(TestCase): 20 | """Tests the tab entity works as expected.""" 21 | 22 | def setUp(self) -> None: 23 | # Close any existing views so as to avoid polluting the results. 24 | for view in sublime.active_window().views(): 25 | view.window().focus_view(view) 26 | view.window().run_command("close_file") 27 | 28 | def tearDown(self) -> None: 29 | for view in sublime.active_window().views(): 30 | view.window().focus_view(view) 31 | view.set_scratch(True) 32 | view.window().run_command("close_file") 33 | 34 | def test_initialisation(self) -> None: 35 | """Test initialising a Tab.""" 36 | dir: str = path.dirname(__file__) 37 | 38 | fixture: str = path.normpath( 39 | path.join(dir, "./fixtures/foo.txt") 40 | ) 41 | scratch_view: sublime.View = sublime.active_window().new_file() 42 | file_view: sublime.View = sublime.active_window().open_file(fixture) 43 | 44 | dataset: Tuple[Tuple[sublime.View, str, bool, Optional[str]], ...] = ( 45 | (scratch_view, "untitled", False, ""), 46 | (file_view, path.basename(fixture), True, path.dirname(fixture)) 47 | ) 48 | 49 | for (view, name, is_file, pathname) in dataset: 50 | with self.subTest( 51 | view=view, 52 | name=name, 53 | is_file=is_file, 54 | pathname=pathname 55 | ): 56 | entity: Tab = Tab(view) 57 | self.assertEquals(name, entity.get_title()) 58 | self.assertEquals(bool(is_file), entity.is_file_view()) 59 | self.assertEquals(pathname, entity.get_path()) 60 | 61 | def test_get_title(self) -> None: 62 | """Tests getting the title of the Tab.""" 63 | scratch_view: sublime.View = sublime.active_window().new_file() 64 | 65 | entity: Tab = Tab(scratch_view) 66 | 67 | self.assertEquals("untitled", entity.get_title()) 68 | 69 | def test_get_subtitle(self) -> None: 70 | """Tests getting the subtitle of the Tab.""" 71 | scratch_view: sublime.View = sublime.active_window().new_file() 72 | 73 | entity: Tab = Tab(scratch_view) 74 | 75 | self.assertEquals("untitled", entity.get_subtitle()) 76 | 77 | def test_set_title(self) -> None: 78 | """Tests setting the title of the Tab.""" 79 | scratch_view: sublime.View = sublime.active_window().new_file() 80 | 81 | entity: Tab = Tab(scratch_view) 82 | 83 | self.assertEquals("untitled", entity.get_title()) 84 | entity.set_title("foo") 85 | self.assertEquals("foo", entity.get_title()) 86 | 87 | def test_set_subtitle(self) -> None: 88 | """Tests setting the subtitle of the Tab.""" 89 | scratch_view: sublime.View = sublime.active_window().new_file() 90 | 91 | entity: Tab = Tab(scratch_view) 92 | 93 | self.assertEquals("untitled", entity.get_subtitle()) 94 | entity.set_subtitle("foo") 95 | self.assertEquals("foo", entity.get_subtitle()) 96 | 97 | def test_is_file_view(self) -> None: 98 | """Tests checking whether the Tab's view is a file or not.""" 99 | scratch_view: sublime.View = sublime.active_window().new_file() 100 | 101 | entity: Tab = Tab(scratch_view) 102 | 103 | self.assertEquals(False, entity.is_file_view()) 104 | 105 | dir: str = path.dirname(__file__) 106 | 107 | fixture: str = path.normpath( 108 | path.join(dir, "./fixtures/foo.txt") 109 | ) 110 | 111 | file_view: sublime.View = sublime.active_window().open_file(fixture) 112 | 113 | entity = Tab(file_view) 114 | self.assertEquals(True, entity.is_file_view()) 115 | 116 | def test_get_path(self) -> None: 117 | """Tests getting the path for a Tab.""" 118 | scratch_view: sublime.View = sublime.active_window().new_file() 119 | 120 | entity: Tab = Tab(scratch_view) 121 | 122 | self.assertEquals("", entity.get_path()) 123 | 124 | dir: str = path.dirname(__file__) 125 | 126 | fixture: str = path.normpath( 127 | path.join(dir, "./fixtures/foo.txt") 128 | ) 129 | 130 | file_view: sublime.View = sublime.active_window().open_file(fixture) 131 | 132 | entity = Tab(file_view) 133 | expected: str = path.dirname(fixture) 134 | self.assertEquals(expected, entity.get_path()) 135 | 136 | def test_get_view(self) -> None: 137 | """Tests getting the underlying view for a Tab.""" 138 | scratch_view: sublime.View = sublime.active_window().new_file() 139 | 140 | entity: Tab = Tab(scratch_view) 141 | 142 | self.assertIs(scratch_view, entity.get_view()) 143 | 144 | def test_add_caption(self) -> None: 145 | """Test adding captions to a Tab.""" 146 | scratch_view: sublime.View = sublime.active_window().new_file() 147 | 148 | entity: Tab = Tab(scratch_view) 149 | 150 | # Ensure we start with no captions. 151 | self.assertListEqual([], entity.get_captions()) 152 | 153 | entity.add_caption("bar") 154 | 155 | # Ensure a regular caption can be added. 156 | self.assertListEqual(["bar"], entity.get_captions()) 157 | 158 | entity.add_caption("baz") 159 | 160 | # Ensure additional captions can be added. 161 | self.assertListEqual(["bar", "baz"], entity.get_captions()) 162 | 163 | second_scratch_view: sublime.View = sublime.active_window().new_file() 164 | 165 | entity = Tab(second_scratch_view) 166 | 167 | entity.add_caption(123) # type: ignore 168 | 169 | # Ensure captions are stringified 170 | self.assertListEqual(["123"], entity.get_captions()) 171 | 172 | def test_get_captions(self) -> None: 173 | """Tests getting the captions for a Tab""" 174 | scratch_view: sublime.View = sublime.active_window().new_file() 175 | entity: Tab = Tab(scratch_view) 176 | 177 | self.assertListEqual([], entity.get_captions()) 178 | entity.add_caption("test") 179 | self.assertListEqual(["test"], entity.get_captions()) 180 | 181 | def test_get_details_caption_configuration(self) -> None: 182 | """Test getting details for a Tab with various caption settings.""" 183 | scratch_view: sublime.View = sublime.active_window().new_file() 184 | 185 | entity: Tab = Tab(scratch_view) 186 | 187 | details: List[str] = entity.get_details() 188 | 189 | # Without captions at all. 190 | self.assertListEqual(["untitled", "untitled"], details) 191 | 192 | details = entity.get_details() 193 | 194 | # With empty captions. 195 | self.assertListEqual(["untitled", "untitled"], details) 196 | 197 | entity.add_caption("bar") 198 | 199 | # With bespoke captions. 200 | details = entity.get_details() 201 | 202 | self.assertListEqual(["untitled", "untitled", "bar"], details) 203 | 204 | entity.add_caption("baz") 205 | 206 | details = entity.get_details() 207 | 208 | self.assertListEqual(["untitled", "untitled", "bar, baz"], details) 209 | 210 | def test_equality_check(self) -> None: 211 | """Tests comparing two tabs for equality.""" 212 | scratch_view: sublime.View = sublime.active_window().new_file() 213 | 214 | t1: Tab = Tab(scratch_view) 215 | t2: Tab = Tab(scratch_view) 216 | 217 | self.assertEquals(t1, t2) 218 | 219 | t3: Tab = Tab(scratch_view) 220 | t3.add_caption("Force a difference") 221 | 222 | self.assertNotEqual(t1, t3) 223 | 224 | def test_to_string(self) -> None: 225 | """Tests representing a tab as a string""" 226 | scratch_view: sublime.View = sublime.active_window().new_file() 227 | 228 | entity: Tab = Tab(scratch_view) 229 | 230 | self.assertEquals(entity.get_title(), str(entity)) 231 | -------------------------------------------------------------------------------- /tests/test_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | import sublime # type: ignore 5 | from unittesting import DeferrableTestCase # type: ignore 6 | from os import path 7 | from typing import List, Tuple, Dict, Generator 8 | try: 9 | from lib import settings, entities 10 | except ImportError: 11 | # If we're running these tests in UnitTesting, then we need to use 12 | # The package name - Tab Filter - so let's grab import lib and try again. 13 | from importlib import import_module 14 | settings = import_module(".lib.settings", "Tab Filter") 15 | entities = import_module(".lib.entities", "Tab Filter") 16 | 17 | TabSetting = settings.TabSetting 18 | ShowCaptionsTabSetting = settings.ShowCaptionsTabSetting 19 | IncludePathTabSetting = settings.IncludePathTabSetting 20 | ShowGroupCaptionTabSetting = settings.ShowGroupCaptionTabSetting 21 | Tab = entities.Tab 22 | 23 | DEFAULT_SETINGS = settings.DEFAULT_SETINGS 24 | 25 | 26 | class BaseSettingsTestCase(DeferrableTestCase): 27 | """Base settings test case to set up boiler plate methods.""" 28 | settings: sublime.Settings 29 | layout: Dict[str, List] 30 | 31 | def setUp(self) -> None: 32 | self.settings = sublime.load_settings("tabfilter.sublime-settings") 33 | self.layout = sublime.active_window().layout() 34 | for setting in DEFAULT_SETINGS: 35 | self.settings.set(setting, DEFAULT_SETINGS[setting]) 36 | 37 | # Close any existing views so as to avoid polluting the results. 38 | for view in sublime.active_window().views(): 39 | view.window().focus_view(view) 40 | view.window().run_command("close_file") 41 | 42 | def tearDown(self) -> None: 43 | # Restore the original layout 44 | sublime.active_window().set_layout(self.layout) 45 | 46 | for view in sublime.active_window().views(): 47 | view.window().focus_view(view) 48 | view.set_scratch(True) 49 | view.window().run_command("close_file") 50 | 51 | 52 | class DefaultSettingsTestCase(BaseSettingsTestCase): 53 | def test_defaults(self) -> None: 54 | """Tests that the default settings are honoured.""" 55 | 56 | scratch_view: sublime.View = sublime.active_window().new_file() 57 | tabs: List[Tab] = [Tab(scratch_view)] 58 | 59 | data_set: Tuple[Tuple[TabSetting, bool, str], ...] = ( 60 | ( 61 | ShowCaptionsTabSetting, 62 | DEFAULT_SETINGS["show_captions"], 63 | "show_captions" 64 | ), 65 | ( 66 | IncludePathTabSetting, 67 | DEFAULT_SETINGS["include_path"], 68 | "include_path" 69 | ), 70 | ( 71 | ShowGroupCaptionTabSetting, 72 | DEFAULT_SETINGS["show_group_caption"], 73 | "show_group_caption" 74 | ) 75 | ) 76 | 77 | for (cls, enabled, caption) in data_set: 78 | with self.subTest(cls=cls, enabled=enabled, caption=caption): 79 | inst = cls( 80 | self.settings, 81 | sublime.active_window() 82 | ) # type: ignore 83 | self.assertEqual(enabled, inst.is_enabled()) 84 | self.assertListEqual(tabs, inst.apply(tabs)) 85 | 86 | 87 | class ShowCaptionsTabSettingTestCase(BaseSettingsTestCase): 88 | """Tests the Show Captions Tab Settings.""" 89 | 90 | def test_setting_disabled(self) -> None: 91 | """Tests with the setting disabled.""" 92 | self.settings.set("show_captions", False) 93 | setting: ShowCaptionsTabSetting = ShowCaptionsTabSetting( 94 | self.settings, 95 | sublime.active_window() 96 | ) 97 | scratch_view: sublime.View = sublime.active_window().new_file() 98 | tabs: List[Tab] = [Tab(scratch_view)] 99 | 100 | self.assertFalse(setting.is_enabled()) 101 | self.assertListEqual(tabs, setting.apply(tabs)) 102 | self.assertListEqual([], tabs[0].get_captions()) 103 | 104 | def test_current_file(self) -> Generator[int, None, None]: 105 | """Tests detecting current file.""" 106 | setting: ShowCaptionsTabSetting = ShowCaptionsTabSetting( 107 | self.settings, 108 | sublime.active_window() 109 | ) 110 | 111 | dir: str = path.dirname(__file__) 112 | 113 | foo_fixture: str = path.normpath( 114 | path.join(dir, "./fixtures/foo.txt") 115 | ) 116 | bar_fixture: str = path.normpath( 117 | path.join(dir, "./fixtures/bar.txt") 118 | ) 119 | 120 | foo_view: sublime.View = sublime.active_window().open_file(foo_fixture) 121 | bar_view: sublime.View = sublime.active_window().open_file(bar_fixture) 122 | 123 | data_set: Tuple[Tuple[List[Tab], List[List[str]], sublime.View], ...] 124 | 125 | data_set = ( 126 | ([Tab(foo_view), Tab(bar_view)], [['Current File'], []], foo_view), 127 | ([Tab(foo_view), Tab(bar_view)], [[], ['Current File']], bar_view), 128 | ) 129 | 130 | yield 100 131 | 132 | for (tabs, captions, view) in data_set: 133 | with self.subTest(tabs=tabs, captions=view, view=view): 134 | view.window().focus_view(view) 135 | self.assertTrue(setting.is_enabled()) 136 | self.assertListEqual(tabs, setting.apply(tabs)) 137 | actual = [] 138 | for tab in tabs: 139 | actual.append(tab.get_captions()) 140 | self.assertListEqual(captions, actual) 141 | 142 | def test_unsaved_file(self) -> None: 143 | """Tests detecting unsaved files.""" 144 | setting: ShowCaptionsTabSetting = ShowCaptionsTabSetting( 145 | self.settings, 146 | sublime.active_window() 147 | ) 148 | scratch_view: sublime.View = sublime.active_window().new_file() 149 | tabs: List[Tab] = [Tab(scratch_view)] 150 | 151 | self.assertTrue(setting.is_enabled()) 152 | self.assertListEqual(tabs, setting.apply(tabs)) 153 | self.assertListEqual( 154 | ["Current File", "Unsaved File"], 155 | tabs[0].get_captions() 156 | ) 157 | 158 | def test_unsaved_changes(self) -> Generator[int, None, None]: 159 | """Tests detecting unsaved changes.""" 160 | setting: ShowCaptionsTabSetting = ShowCaptionsTabSetting( 161 | self.settings, 162 | sublime.active_window() 163 | ) 164 | 165 | dir: str = path.dirname(__file__) 166 | 167 | foo_fixture: str = path.normpath( 168 | path.join(dir, "./fixtures/foo.txt") 169 | ) 170 | 171 | foo_view: sublime.View = sublime.active_window().open_file(foo_fixture) 172 | 173 | sublime.set_timeout( 174 | lambda: foo_view.run_command("insert", {"characters": "foo"}), 175 | 100 176 | ) 177 | 178 | yield 100 179 | 180 | tabs: List[Tab] = [Tab(foo_view)] 181 | 182 | self.assertTrue(setting.is_enabled()) 183 | self.assertListEqual(tabs, setting.apply(tabs)) 184 | self.assertListEqual( 185 | ["Current File", "Unsaved Changes"], 186 | tabs[0].get_captions() 187 | ) 188 | 189 | def test_read_only(self) -> None: 190 | """Tests detecting read only views.""" 191 | setting: ShowCaptionsTabSetting = ShowCaptionsTabSetting( 192 | self.settings, 193 | sublime.active_window() 194 | ) 195 | scratch_view: sublime.View = sublime.active_window().new_file() 196 | scratch_view.set_read_only(True) 197 | tabs: List[Tab] = [Tab(scratch_view)] 198 | 199 | self.assertTrue(setting.is_enabled()) 200 | self.assertListEqual(tabs, setting.apply(tabs)) 201 | self.assertListEqual( 202 | ["Current File", "Unsaved File", "Read Only"], 203 | tabs[0].get_captions() 204 | ) 205 | 206 | 207 | class IncludePathTabSettingTestCase(BaseSettingsTestCase): 208 | """Tests the Include path Tab Settings.""" 209 | 210 | def test_setting_disabled(self) -> Generator[int, None, None]: 211 | """Tests with the setting disabled.""" 212 | self.settings.set("include_path", False) 213 | setting: IncludePathTabSetting = IncludePathTabSetting( 214 | self.settings, 215 | sublime.active_window() 216 | ) 217 | 218 | dir: str = path.dirname(__file__) 219 | 220 | foo_fixture: str = path.normpath( 221 | path.join(dir, "./fixtures/foo.txt") 222 | ) 223 | 224 | expected = path.basename(foo_fixture) 225 | 226 | foo_view: sublime.View = sublime.active_window().open_file(foo_fixture) 227 | tabs: List[Tab] = [Tab(foo_view)] 228 | 229 | yield 100 230 | 231 | self.assertFalse(setting.is_enabled()) 232 | self.assertListEqual(tabs, setting.apply(tabs)) 233 | self.assertEqual(expected, tabs[0].get_title()) 234 | self.assertEqual(foo_fixture, tabs[0].get_subtitle()) 235 | 236 | def test_with_file_view(self) -> Generator[int, None, None]: 237 | """Tests with the setting disabled.""" 238 | self.settings.set("include_path", True) 239 | setting: IncludePathTabSetting = IncludePathTabSetting( 240 | self.settings, 241 | sublime.active_window() 242 | ) 243 | 244 | dir: str = path.dirname(__file__) 245 | 246 | foo_fixture: str = path.normpath( 247 | path.join(dir, "./fixtures/foo.txt") 248 | ) 249 | 250 | foo_view: sublime.View = sublime.active_window().open_file(foo_fixture) 251 | tabs: List[Tab] = [Tab(foo_view)] 252 | 253 | yield 100 254 | 255 | self.assertTrue(setting.is_enabled()) 256 | self.assertListEqual(tabs, setting.apply(tabs)) 257 | self.assertEqual(foo_fixture, tabs[0].get_title()) 258 | self.assertEqual(foo_fixture, tabs[0].get_subtitle()) 259 | 260 | 261 | class ShowGroupCaptionsTabSettingTestCase(BaseSettingsTestCase): 262 | """Tests the Show Group Captions Tab Settings.""" 263 | 264 | def test_setting_disabled(self) -> None: 265 | """Tests with the setting disabled.""" 266 | self.settings.set("show_group_caption", False) 267 | setting: ShowGroupCaptionTabSetting = ShowGroupCaptionTabSetting( 268 | self.settings, 269 | sublime.active_window() 270 | ) 271 | scratch_view: sublime.View = sublime.active_window().new_file() 272 | tabs: List[Tab] = [Tab(scratch_view)] 273 | 274 | self.assertFalse(setting.is_enabled()) 275 | self.assertListEqual(tabs, setting.apply(tabs)) 276 | self.assertListEqual([], tabs[0].get_captions()) 277 | 278 | def test_single_group(self) -> None: 279 | """Tests applying to a single group (no caption expected).""" 280 | self.settings.set("show_group_caption", True) 281 | setting: ShowGroupCaptionTabSetting = ShowGroupCaptionTabSetting( 282 | self.settings, 283 | sublime.active_window() 284 | ) 285 | scratch_view: sublime.View = sublime.active_window().new_file() 286 | tabs: List[Tab] = [Tab(scratch_view)] 287 | 288 | # single column layout 289 | layout: Dict[str, List] = { 290 | "cells": [[0, 0, 1, 1]], 291 | "cols": [0.0, 1.0], 292 | "rows": [0.0, 1.0] 293 | } 294 | 295 | sublime.active_window().set_layout(layout) 296 | 297 | self.assertFalse(setting.is_enabled()) 298 | self.assertListEqual(tabs, setting.apply(tabs)) 299 | self.assertListEqual([], tabs[0].get_captions()) 300 | 301 | def test_multiple_groups(self) -> None: 302 | """Tests applying to multiple groups.""" 303 | self.settings.set("show_group_caption", True) 304 | setting: ShowGroupCaptionTabSetting = ShowGroupCaptionTabSetting( 305 | self.settings, 306 | sublime.active_window() 307 | ) 308 | scratch_view: sublime.View = sublime.active_window().new_file() 309 | second_view: sublime.View = sublime.active_window().new_file() 310 | 311 | # 2 column layout 312 | layout: Dict[str, List] = { 313 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 314 | "cols": [0.0, 0.5, 1.0], 315 | "rows": [0.0, 1.0] 316 | } 317 | 318 | sublime.active_window().set_layout(layout) 319 | sublime.active_window().set_view_index(scratch_view, group=0, idx=0) 320 | sublime.active_window().set_view_index(second_view, group=1, idx=0) 321 | 322 | tabs: List[Tab] = [Tab(scratch_view), Tab(second_view)] 323 | 324 | self.assertTrue(setting.is_enabled()) 325 | self.assertListEqual(tabs, setting.apply(tabs)) 326 | captions: List[List[str]] = [tab.get_captions() for tab in tabs] 327 | self.assertListEqual([["Group: 1"], ["Group: 2"]], captions) 328 | -------------------------------------------------------------------------------- /tests/test_tabfilter.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013 - 2021 Robin Malburn 2 | # See the file license.txt for copying permission. 3 | 4 | import sublime # type: ignore 5 | from unittesting import DeferrableTestCase # type: ignore 6 | from os import path 7 | from unittest.mock import patch 8 | from typing import List, Dict, Generator 9 | try: 10 | import tabfilter 11 | from lib import settings, entities 12 | except ImportError: 13 | # If we're running these tests in UnitTesting, then we need to use 14 | # The package name - Tab Filter - so let's grab import lib and try again. 15 | from importlib import import_module 16 | tabfilter = import_module(".tabfilter", "Tab Filter") 17 | settings = import_module(".lib.settings", "Tab Filter") 18 | entities = import_module(".lib.entities", "Tab Filter") 19 | 20 | TabFilterCommand = tabfilter.TabFilterCommand 21 | 22 | DEFAULT_SETINGS = settings.DEFAULT_SETINGS 23 | 24 | 25 | class TabFilterCommandTestCase(DeferrableTestCase): 26 | """Tests the tab filter command works as expected.""" 27 | 28 | settings: sublime.Settings 29 | layout: Dict[str, List] 30 | 31 | def setUp(self) -> None: 32 | self.settings = sublime.load_settings("tabfilter.sublime-settings") 33 | self.layout = sublime.active_window().layout() 34 | for setting in DEFAULT_SETINGS: 35 | self.settings.set(setting, DEFAULT_SETINGS[setting]) 36 | 37 | # Close any existing views so as to avoid polluting the results. 38 | for view in sublime.active_window().views(): 39 | view.window().focus_view(view) 40 | view.window().run_command("close_file") 41 | 42 | def tearDown(self) -> None: 43 | # Restore the original layout 44 | sublime.active_window().set_layout(self.layout) 45 | 46 | for view in sublime.active_window().views(): 47 | view.window().focus_view(view) 48 | view.set_scratch(True) 49 | view.window().run_command("close_file") 50 | 51 | def test_gather_tabs_no_files(self) -> None: 52 | """Tests gathering tabs when there are no files.""" 53 | window: sublime.Window = sublime.active_window() 54 | groups: List[int] = list(range(window.num_groups())) 55 | cmd: TabFilterCommand = TabFilterCommand(window) 56 | 57 | self.assertListEqual( 58 | [], 59 | cmd.gather_tabs(groups) 60 | ) 61 | 62 | def test_gather_tabs_single_group(self) -> None: 63 | """Tests gathering tabs from a single group layout.""" 64 | window: sublime.Window = sublime.active_window() 65 | view: sublime.View = window.new_file() 66 | view.set_scratch(True) 67 | groups: List[int] = list(range(window.num_groups())) 68 | 69 | cmd: TabFilterCommand = TabFilterCommand(window) 70 | 71 | self.assertListEqual( 72 | [entities.Tab(view)], 73 | cmd.gather_tabs(groups) 74 | ) 75 | 76 | def test_gather_tabs_multiple_groups(self) -> None: 77 | """Tests gathering tabs from a multi-group layout.""" 78 | window: sublime.Window = sublime.active_window() 79 | view: sublime.View = window.new_file() 80 | view.set_scratch(True) 81 | second_view: sublime.View = window.new_file() 82 | second_view.set_scratch(True) 83 | 84 | # 2 column layout 85 | layout: Dict[str, List] = { 86 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 87 | "cols": [0.0, 0.5, 1.0], 88 | "rows": [0.0, 1.0] 89 | } 90 | 91 | sublime.active_window().set_layout(layout) 92 | sublime.active_window().set_view_index(view, group=0, idx=0) 93 | sublime.active_window().set_view_index(second_view, group=1, idx=0) 94 | 95 | cmd: TabFilterCommand = TabFilterCommand(window) 96 | 97 | groups: List[int] = list(range(window.num_groups())) 98 | 99 | self.assertListEqual( 100 | [entities.Tab(view), entities.Tab(second_view)], 101 | cmd.gather_tabs(groups) 102 | ) 103 | 104 | def test_gather_tabs_selected_group(self) -> None: 105 | """Tests gathering tabs from the selected layout.""" 106 | window: sublime.Window = sublime.active_window() 107 | view: sublime.View = window.new_file() 108 | view.set_scratch(True) 109 | second_view: sublime.View = window.new_file() 110 | second_view.set_scratch(True) 111 | 112 | # 2 column layout 113 | layout: Dict[str, List] = { 114 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 115 | "cols": [0.0, 0.5, 1.0], 116 | "rows": [0.0, 1.0] 117 | } 118 | 119 | sublime.active_window().set_layout(layout) 120 | sublime.active_window().set_view_index(second_view, group=1, idx=0) 121 | 122 | cmd: TabFilterCommand = TabFilterCommand(window) 123 | 124 | groups: List[int] = [second_view.sheet().group()] 125 | 126 | self.assertListEqual( 127 | [entities.Tab(second_view)], 128 | cmd.gather_tabs(groups) 129 | ) 130 | 131 | def test_format_tabs(self) -> None: 132 | """Tests formatting tabs.""" 133 | window: sublime.Window = sublime.active_window() 134 | view: sublime.View = window.new_file() 135 | view.set_scratch(True) 136 | 137 | cmd: TabFilterCommand = TabFilterCommand(window) 138 | 139 | tabs: List[entities.Tab] = [entities.Tab(view)] 140 | 141 | with patch.object( 142 | settings.CommonPrefixTabSetting, 143 | "apply", 144 | return_value=tabs 145 | ) as mock_apply: 146 | setting: settings.CommonPrefixTabSetting 147 | 148 | setting = settings.CommonPrefixTabSetting( 149 | self.settings, 150 | sublime.active_window() 151 | ) 152 | 153 | details: List[List[str]] = [tab.get_details() for tab in tabs] 154 | 155 | self.assertEqual( 156 | details, 157 | cmd.format_tabs(tabs, (setting,)) 158 | ) 159 | mock_apply.assert_called_once_with(tabs) 160 | 161 | def test_display_quick_info_no_preview(self) -> None: 162 | """Tests displaying the quick info panel, without preview.""" 163 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 164 | window: sublime.Window = sublime.active_window() 165 | 166 | cmd: TabFilterCommand = TabFilterCommand(window) 167 | 168 | tabs: List[List[str]] = [["untitled", "untitled"]] 169 | 170 | cmd.display_quick_info_panel(tabs, preview=False) 171 | 172 | mock_panel.assert_called_once_with(tabs, cmd.on_done) 173 | 174 | def test_display_quick_info_with_preview(self) -> None: 175 | """Tests displaying the quick info panel, with preview.""" 176 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 177 | window: sublime.Window = sublime.active_window() 178 | 179 | cmd: TabFilterCommand = TabFilterCommand(window) 180 | 181 | tabs: List[List[str]] = [["untitled", "untitled"]] 182 | 183 | cmd.display_quick_info_panel(tabs, preview=True) 184 | 185 | mock_panel.assert_called_once_with( 186 | tabs, 187 | cmd.on_done, 188 | on_highlight=cmd.on_highlighted, 189 | selected_index=-1 190 | ) 191 | 192 | @patch.object(settings.CommonPrefixTabSetting, "apply") 193 | @patch.object(settings.ShowGroupCaptionTabSetting, "apply") 194 | @patch.object(settings.ShowCaptionsTabSetting, "apply") 195 | @patch.object(settings.IncludePathTabSetting, "apply") 196 | def test_run( 197 | self, 198 | mock_common_prefix_apply, 199 | mock_group_caption_apply, 200 | mock_captions_apply, 201 | mock_include_path_apply 202 | ) -> None: 203 | """Tests the run method with a mocked set up.""" 204 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 205 | window: sublime.Window = sublime.active_window() 206 | view: sublime.View = window.new_file() 207 | view.set_scratch(True) 208 | 209 | tabs: List[entities.Tab] = [entities.Tab(view)] 210 | details: List[List[str]] = [tab.get_details() for tab in tabs] 211 | 212 | mock_common_prefix_apply.return_value = tabs 213 | mock_group_caption_apply.return_value = tabs 214 | mock_captions_apply.return_value = tabs 215 | mock_include_path_apply.return_value = tabs 216 | 217 | cmd: TabFilterCommand = TabFilterCommand(window) 218 | cmd.run() 219 | 220 | mock_common_prefix_apply.assert_called_once_with(tabs) 221 | mock_group_caption_apply.assert_called_once_with(tabs) 222 | mock_captions_apply.assert_called_once_with(tabs) 223 | mock_include_path_apply.assert_called_once_with(tabs) 224 | 225 | mock_panel.assert_called_once_with(details, cmd.on_done) 226 | 227 | @patch.object(settings.CommonPrefixTabSetting, "apply") 228 | @patch.object(settings.ShowGroupCaptionTabSetting, "apply") 229 | @patch.object(settings.ShowCaptionsTabSetting, "apply") 230 | @patch.object(settings.IncludePathTabSetting, "apply") 231 | def test_run_with_no_files( 232 | self, 233 | mock_common_prefix_apply, 234 | mock_group_caption_apply, 235 | mock_captions_apply, 236 | mock_include_path_apply 237 | ) -> None: 238 | """Tests the run method with a mocked set up and no files.""" 239 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 240 | window: sublime.Window = sublime.active_window() 241 | 242 | tabs: List = [] 243 | 244 | mock_common_prefix_apply.return_value = tabs 245 | mock_group_caption_apply.return_value = tabs 246 | mock_captions_apply.return_value = tabs 247 | mock_include_path_apply.return_value = tabs 248 | 249 | cmd: TabFilterCommand = TabFilterCommand(window) 250 | cmd.run() 251 | 252 | mock_common_prefix_apply.assert_called_once_with(tabs) 253 | mock_group_caption_apply.assert_called_once_with(tabs) 254 | mock_captions_apply.assert_called_once_with(tabs) 255 | mock_include_path_apply.assert_called_once_with(tabs) 256 | 257 | mock_panel.assert_called_once_with([], cmd.on_done) 258 | 259 | def test_run_with_scratch_default_settings(self) -> None: 260 | """Test running with a scratch buffer and default settings.""" 261 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 262 | window: sublime.Window = sublime.active_window() 263 | 264 | view: sublime.View = window.new_file() 265 | view.set_scratch(True) 266 | 267 | cmd: TabFilterCommand = TabFilterCommand(window) 268 | cmd.run() 269 | 270 | expected: List[str] = [ 271 | "untitled", 272 | "untitled", 273 | "Current File, Unsaved File" 274 | ] 275 | 276 | mock_panel.assert_called_once_with([expected], cmd.on_done) 277 | 278 | def test_run_with_file_default_settings(self) -> Generator[int, None, None]: 279 | """Test running with a file and default settings.""" 280 | dir: str = path.dirname(__file__) 281 | 282 | fixture: str = path.normpath( 283 | path.join(dir, "./fixtures/foo.txt") 284 | ) 285 | 286 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 287 | window: sublime.Window = sublime.active_window() 288 | 289 | window.open_file(fixture) 290 | 291 | cmd: TabFilterCommand = TabFilterCommand(window) 292 | 293 | sublime.set_timeout(lambda: cmd.run(), 100) 294 | 295 | yield 100 296 | 297 | expected: List[str] = [ 298 | "foo.txt", 299 | "...{}foo.txt".format(path.sep), 300 | "Current File" 301 | ] 302 | mock_panel.assert_called_once_with([expected], cmd.on_done) 303 | 304 | def test_run_with_active_group(self) -> Generator[int, None, None]: 305 | """Test running with active group restriction & defaults.""" 306 | dir: str = path.dirname(__file__) 307 | 308 | foo_fixture: str = path.normpath( 309 | path.join(dir, "./fixtures/foo.txt") 310 | ) 311 | 312 | bar_fixture: str = path.normpath( 313 | path.join(dir, "./fixtures/bar.txt") 314 | ) 315 | 316 | with patch.object(sublime.Window, "show_quick_panel") as mock_panel: 317 | window: sublime.Window = sublime.active_window() 318 | 319 | scratch_view: sublime.View = window.new_file() 320 | foo_view: sublime.View = window.open_file(foo_fixture) 321 | bar_view: sublime.View = window.open_file(bar_fixture) 322 | 323 | # 2 column layout 324 | layout: Dict[str, List] = { 325 | "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], 326 | "cols": [0.0, 0.5, 1.0], 327 | "rows": [0.0, 1.0] 328 | } 329 | 330 | window.set_layout(layout) 331 | window.set_view_index(foo_view, group=1, idx=0) 332 | window.set_view_index(scratch_view, group=0, idx=0) 333 | window.set_view_index(bar_view, group=0, idx=0) 334 | 335 | yield 100 336 | 337 | window.focus_view(bar_view) 338 | 339 | cmd: TabFilterCommand = TabFilterCommand(window) 340 | cmd.run(active_group_only=True) 341 | 342 | expected: List[List[str]] = [ 343 | [ 344 | "bar.txt", 345 | "...{}bar.txt".format(path.sep), 346 | "Current File" 347 | ], 348 | [ 349 | "untitled", 350 | "untitled", 351 | "Unsaved File" 352 | ], 353 | ] 354 | 355 | mock_panel.assert_called_once_with(expected, cmd.on_done) 356 | 357 | def test_on_done_callback_with_valid_index(self) -> None: 358 | """Tests the on done callback works with valid selection.""" 359 | index: int = 0 360 | 361 | with patch.object(sublime.Window, "focus_view") as mock_focus_view: 362 | window: sublime.Window = sublime.active_window() 363 | cmd: TabFilterCommand = TabFilterCommand(window) 364 | # add the view to the internal list 365 | cmd.views.append(window.new_file()) 366 | cmd.on_done(index) 367 | 368 | mock_focus_view.assert_called_once_with(cmd.views[index]) 369 | 370 | def test_on_done_callback_with_no_selection(self) -> None: 371 | """Tests the on done callback works with no selection and can restore 372 | the currently selected tab where available. 373 | """ 374 | index: int = -1 375 | 376 | with patch.object(sublime.Window, "focus_view") as mock_focus_view: 377 | window: sublime.Window = sublime.active_window() 378 | cmd: TabFilterCommand = TabFilterCommand(window) 379 | # add the view to the internal list 380 | cmd.views.append(window.new_file()) 381 | cmd.on_done(index) 382 | 383 | mock_focus_view.assert_not_called() 384 | 385 | # now ensure that if a current tab is supported, 386 | # we revert to showing that tab. 387 | cmd.current_tab_idx = 0 388 | cmd.on_done(index) 389 | mock_focus_view.assert_called_once_with(cmd.views[0]) 390 | 391 | def test_on_done_callback_with_invalid_index(self) -> None: 392 | """Tests the on done callback handles invalid data.""" 393 | 394 | with patch.object(sublime.Window, "focus_view") as mock_focus_view: 395 | window: sublime.Window = sublime.active_window() 396 | cmd: TabFilterCommand = TabFilterCommand(window) 397 | # Test values greatly below the minimum expected range. 398 | cmd.on_done(-100) 399 | mock_focus_view.assert_not_called() 400 | 401 | # Also test values greatly outside the expected range. 402 | cmd.on_done(100) 403 | mock_focus_view.assert_not_called() 404 | 405 | def test_on_highlighted_callback_with_valid_index(self) -> None: 406 | """Tests the on highlighted callback works with valid selection.""" 407 | index: int = 0 408 | 409 | with patch.object(sublime.Window, "focus_view") as mock_focus_view: 410 | window: sublime.Window = sublime.active_window() 411 | cmd: TabFilterCommand = TabFilterCommand(window) 412 | # add the view to the internal list 413 | cmd.views.append(window.new_file()) 414 | cmd.on_highlighted(index) 415 | 416 | mock_focus_view.assert_called_once_with(cmd.views[index]) 417 | 418 | def test_on_highlighted_callback_with_invalid_index(self) -> None: 419 | """Tests the on highlighted callback handles invalid data.""" 420 | 421 | with patch.object(sublime.Window, "focus_view") as mock_focus_view: 422 | window: sublime.Window = sublime.active_window() 423 | cmd: TabFilterCommand = TabFilterCommand(window) 424 | # Test values greatly below the minimum expected range. 425 | cmd.on_highlighted(-100) 426 | mock_focus_view.assert_not_called() 427 | 428 | # Also test values greatly outside the expected range. 429 | cmd.on_highlighted(100) 430 | mock_focus_view.assert_not_called() 431 | -------------------------------------------------------------------------------- /unittesting.json: -------------------------------------------------------------------------------- 1 | { 2 | "tests_dir" : "tests", 3 | "pattern" : "test_*.py", 4 | "verbosity": 2, 5 | "output": "", 6 | "capture_console": true, 7 | "deferred": true 8 | } --------------------------------------------------------------------------------