├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── icon.png ├── keymaps └── expose.cson ├── lib ├── expose-tab-view.coffee ├── expose-view.coffee ├── expose.coffee └── file-icons.coffee ├── menus └── expose.cson ├── package.json ├── screenshots └── preview.png ├── spec ├── expose-spec.coffee ├── expose-tab-view-spec.coffee ├── expose-view-spec.coffee └── fixtures │ ├── archive.zip │ ├── sample1.txt │ ├── sample2.txt │ └── sample3.txt └── styles ├── expose.less └── themes.less /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | ss.png 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | 3 | notifications: 4 | email: false 5 | 6 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 7 | 8 | env: 9 | - APM_TEST_PACKAGES="minimap" 10 | 11 | git: 12 | depth: 10 13 | 14 | branches: 15 | only: 16 | - master 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Martin Rodalgaard 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Atom Tabs Exposé [![Build Status](https://travis-ci.org/mrodalgaard/atom-expose.svg)](https://travis-ci.org/mrodalgaard/atom-expose) 2 | 3 | > DEPRECATED: This package will no longer be maintained by me since I have switched permanently to VSCode. 4 | 5 | Quick tab overview of open files. Similar to Mac OSX Exposé / Mission Control, Firefox Tab Group, Safari and Chrome Tab Overview, etc. 6 | 7 | * Shows active tab, panes and previews. 8 | * The text editor preview is taken from [Minimap](https://github.com/atom-minimap/minimap) if present, else a suitable file icon is used. 9 | * Shows file icons in tabs if the [file-icon package](https://github.com/DanBrooker/file-icons) is installed. 10 | 11 | ![expose-package](https://raw.githubusercontent.com/mrodalgaard/atom-expose/master/screenshots/preview.png) 12 | 13 | ## Keymaps 14 | 15 | Toggle expose with cmd-shift-e on Mac OSX and alt-shift-e on Linux and Windows. 16 | 17 | ESC and Enter closes the expose panel. 18 | 19 | 1 - 9 jumps to tabs. 20 | 21 | and select tabs. 22 | 23 | Just start typing to search among the shown tabs. 24 | 25 | ## Tasks 26 | 27 | - [x] Basic exposé functionality 28 | - [x] Activate and close tabs 29 | - [x] Add tests 30 | - [x] Add detailed unit tests 31 | - [x] Tab preview images with fallback 32 | - [x] Show preview of images 33 | - [x] Tab icons 34 | - [x] Custom icons for more view classes 35 | - [x] Keyboard shortcuts (e.g. numbers) 36 | - [x] Move / sortable tabs 37 | - [x] Stay updated on workspace changes 38 | - [x] Show active tab and navigate with arrows 39 | - [ ] Show all project files options 40 | 41 | > Contributions, bug reports and feature requests are very welcome. 42 | 43 | >     _- Martin_ 44 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/icon.png -------------------------------------------------------------------------------- /keymaps/expose.cson: -------------------------------------------------------------------------------- 1 | '.platform-darwin atom-workspace atom-text-editor:not([mini]), .platform-darwin atom-workspace': 2 | 'cmd-shift-e': 'expose:toggle' 3 | 4 | '.platform-win32 atom-workspace, .platform-linux atom-workspace': 5 | 'alt-shift-e': 'expose:toggle' 6 | 7 | '.expose-view': 8 | '1': 'expose:activate-1' 9 | '2': 'expose:activate-2' 10 | '3': 'expose:activate-3' 11 | '4': 'expose:activate-4' 12 | '5': 'expose:activate-5' 13 | '6': 'expose:activate-6' 14 | '7': 'expose:activate-7' 15 | '8': 'expose:activate-8' 16 | '9': 'expose:activate-9' 17 | 'tab': 'core:move-right' 18 | 'shift-tab': 'core:move-left' 19 | -------------------------------------------------------------------------------- /lib/expose-tab-view.coffee: -------------------------------------------------------------------------------- 1 | {View, $$} = require 'atom-space-pen-views' 2 | {CompositeDisposable} = require 'atom' 3 | 4 | FileIcons = require './file-icons' 5 | 6 | module.exports = 7 | class ExposeView extends View 8 | @content: (title, color) -> 9 | @div click: 'activateTab', class: 'expose-tab', => 10 | @div class: 'tab-header', => 11 | @div outlet: 'itemTitle', 'data-name': title, title 12 | @div click: 'closeTab', class: 'close-icon icon-x' 13 | @div outlet: 'tabBody', class: 'tab-body', style: "border-color: #{color}" 14 | 15 | constructor: (@item = {}, @color = '#000') -> 16 | @title = @getItemTitle() 17 | super(@title, @color) 18 | 19 | initialize: -> 20 | @disposables = new CompositeDisposable 21 | @handleEvents() 22 | @populateTabBody() 23 | @updateIcon() 24 | 25 | handleEvents: -> 26 | @on 'click', '.icon-sync', @refreshTab 27 | 28 | @disposables.add atom.commands.add @element, 29 | 'expose:close-tab': (e) => @closeTab(e) 30 | 31 | @disposables.add atom.workspace.observeActivePaneItem @toggleActive 32 | 33 | destroy: -> 34 | @remove() 35 | @disposables?.dispose() 36 | 37 | populateTabBody: -> 38 | return if @drawImage() 39 | return if @drawMinimap() 40 | @drawFallback() 41 | 42 | drawFallback: -> 43 | objectClass = @item.constructor.name 44 | iconClass = 'icon-' + @item.getIconName() if @item.getIconName 45 | @tabBody.html $$ -> 46 | @a class: iconClass or switch objectClass 47 | when 'TextEditor' then 'icon-file-code' 48 | when 'ArchiveEditor' then 'icon-file-zip' 49 | when 'ArchiveEditorView' then 'icon-file-zip' 50 | else 'icon-file-text' 51 | 52 | drawImage: -> 53 | return unless @item.constructor.name is 'ImageEditor' 54 | filePath = @item.file.path 55 | @tabBody.html $$ -> 56 | @img src: filePath 57 | 58 | drawMinimap: -> 59 | return unless @item.constructor.name is 'TextEditor' 60 | return unless atom.packages.loadedPackages.minimap 61 | 62 | atom.packages.serviceHub.consume 'minimap', '1.0.0', (minimapAPI) => 63 | if minimapAPI.standAloneMinimapForEditor? 64 | minimap = minimapAPI.standAloneMinimapForEditor(@item) 65 | minimapElement = atom.views.getView(minimap) 66 | minimapElement.style.cssText = ''' 67 | width: 190px; 68 | height: 130px; 69 | left: 10px; 70 | pointer-events: none; 71 | position: absolute; 72 | ''' 73 | 74 | minimap.setCharWidth?(2) 75 | minimap.setCharHeight?(4) 76 | minimap.setInterline?(2) 77 | 78 | @tabBody.html minimapElement 79 | else 80 | @tabBody.html $$ -> 81 | @a class: 'icon-sync' 82 | 83 | refreshTab: (event) => 84 | event.stopPropagation() 85 | event.target.className += ' animate' 86 | atom.workspace.paneForItem(@item).activateItem(@item) 87 | setTimeout (=> @populateTabBody()), 1000 88 | 89 | activateTab: -> 90 | pane = atom.workspace.paneForItem(@item) 91 | pane.activate() 92 | pane.activateItem(@item) 93 | 94 | toggleActive: (item) => 95 | @toggleClass('active', item is @item) 96 | 97 | isActiveTab: -> 98 | atom.workspace.getActivePaneItem() is @item 99 | 100 | closeTab: (event) -> 101 | event?.stopPropagation() 102 | atom.workspace.paneForItem(@item).destroyItem(@item) 103 | @destroy() 104 | 105 | getItemTitle: -> 106 | return 'untitled' unless title = @item.getTitle?() 107 | 108 | for paneItem in atom.workspace.getPaneItems() when paneItem isnt @item 109 | if paneItem.getTitle() is title and @item.getLongTitle? 110 | title = @item.getLongTitle() 111 | title 112 | 113 | isItemPending: -> 114 | return false unless pane = atom.workspace.paneForItem(@item) 115 | if pane.getPendingItem? 116 | pane.getPendingItem() is @item 117 | else if @item.isPending? 118 | @item.isPending() 119 | 120 | updateIcon: -> 121 | classList = 'title ' 122 | classList += 'pending ' if @isItemPending() 123 | 124 | if iconName = @item.getIconName?() 125 | classList += "icon-#{iconName}" 126 | else if path = @item.getPath?() 127 | if iconName = FileIcons.getService().iconClassForPath(path, 'expose') 128 | classList += iconName.join(' ') 129 | 130 | @itemTitle.attr('class', classList) 131 | -------------------------------------------------------------------------------- /lib/expose-view.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, TextBuffer} = require 'atom' 2 | {View, TextEditorView} = require 'atom-space-pen-views' 3 | {filter} = require 'fuzzaldrin' 4 | Sortable = require 'sortablejs' 5 | 6 | ExposeTabView = require './expose-tab-view' 7 | 8 | module.exports = 9 | class ExposeView extends View 10 | tabs: [] 11 | 12 | @content: (searchBuffer) -> 13 | searchTextEditor = atom.workspace.buildTextEditor( 14 | mini: true 15 | tabLength: 2 16 | softTabs: true 17 | softWrapped: false 18 | buffer: searchBuffer 19 | placeholderText: 'Search tabs' 20 | ) 21 | 22 | @div class: 'expose-view', tabindex: -1, => 23 | @div class: 'expose-top input-block', => 24 | @div class: 'input-block-item input-block-item--flex', => 25 | @subview 'searchView', new TextEditorView(editor: searchTextEditor) 26 | @div class: 'input-block-item', => 27 | @div class: 'btn-group', => 28 | @button outlet: 'exposeSettings', class: 'btn icon-gear' 29 | @button class: 'btn icon-x' 30 | 31 | @div outlet: 'tabList', class: 'tab-list' 32 | 33 | constructor: () -> 34 | super @searchBuffer = new TextBuffer 35 | 36 | initialize: -> 37 | @disposables = new CompositeDisposable 38 | @handleEvents() 39 | @handleDrag() 40 | 41 | destroy: -> 42 | @remove() 43 | @disposables?.dispose() 44 | 45 | handleEvents: -> 46 | @exposeSettings.on 'click', -> 47 | atom.workspace.open 'atom://config/packages/expose' 48 | 49 | @searchView.on 'click', (event) -> 50 | event.stopPropagation() 51 | 52 | @searchView.getModel().onDidStopChanging => 53 | @update() if @didIgnoreFirstChange 54 | @didIgnoreFirstChange = true 55 | 56 | # This event gets propagated from most element clicks on top 57 | @on 'click', (event) => 58 | event.stopPropagation() 59 | @exposeHide() 60 | 61 | @disposables.add atom.commands.add @element, 62 | 'core:confirm': => @handleConfirm() 63 | 'core:cancel': => @exposeHide() 64 | 'core:move-right': => @nextTab() 65 | 'core:move-left': => @nextTab(-1) 66 | 'expose:close': => @exposeHide() 67 | 'expose:activate-1': => @handleNumberKey(1) 68 | 'expose:activate-2': => @handleNumberKey(2) 69 | 'expose:activate-3': => @handleNumberKey(3) 70 | 'expose:activate-4': => @handleNumberKey(4) 71 | 'expose:activate-5': => @handleNumberKey(5) 72 | 'expose:activate-6': => @handleNumberKey(6) 73 | 'expose:activate-7': => @handleNumberKey(7) 74 | 'expose:activate-8': => @handleNumberKey(8) 75 | 'expose:activate-9': => @handleNumberKey(9) 76 | 77 | @on 'keydown', (event) => @handleKeyEvent(event) 78 | 79 | @disposables.add atom.workspace.onDidAddPaneItem => @update() 80 | @disposables.add atom.workspace.onDidDestroyPaneItem => @update() 81 | 82 | handleDrag: -> 83 | Sortable.create( 84 | @tabList.context 85 | ghostClass: 'ghost' 86 | onEnd: (evt) => @moveTab(evt.oldIndex, evt.newIndex) 87 | ) 88 | 89 | moveTab: (from, to) -> 90 | return unless fromItem = @tabs[from]?.item 91 | return unless toItem = @tabs[to]?.item 92 | 93 | fromPane = atom.workspace.paneForItem(fromItem) 94 | toPane = atom.workspace.paneForItem(toItem) 95 | 96 | toPaneIndex = 0 97 | for item, i in toPane.getItems() 98 | toPaneIndex = i if item is toItem 99 | 100 | fromPane.moveItemToPane(fromItem, toPane, toPaneIndex) 101 | @update(true) 102 | 103 | didChangeVisible: (@visible) -> 104 | if @visible 105 | @searchBuffer.setText('') 106 | @update() 107 | @focus() 108 | else 109 | atom.workspace.getActivePane().activate() 110 | 111 | getGroupColor: (n) -> 112 | colors = ['#3498db', '#e74c3c', '#2ecc71', '#9b59b6'] 113 | colors[n % colors.length] 114 | 115 | update: (force) -> 116 | return unless @visible or force 117 | @removeTabs() 118 | 119 | @tabs = [] 120 | for pane, i in @getPanes() 121 | color = @getGroupColor(i) 122 | for item in pane.getItems() 123 | @tabs.push new ExposeTabView(item, color) 124 | 125 | @renderTabs(@tabs = @filterTabs(@tabs)) 126 | 127 | getPanes: -> 128 | atom.workspace.getCenter().getPanes() 129 | 130 | filterTabs: (tabs) -> 131 | text = @searchBuffer.getText() 132 | return tabs if text is '' 133 | filter(tabs, text, key: 'title') 134 | 135 | renderTabs: (tabs) -> 136 | for tab in tabs 137 | @tabList.append tab 138 | 139 | removeTabs: -> 140 | @tabList.empty() 141 | for tab in @tabs 142 | tab.destroy() 143 | @tabs = [] 144 | 145 | activateTab: (n = 1) -> 146 | n = 1 if n < 1 147 | n = @tabs.length if n > 9 or n > @tabs.length 148 | @tabs[n-1]?.activateTab() 149 | @exposeHide() 150 | 151 | handleConfirm: -> 152 | if @isSearching() then @activateTab() else @exposeHide() 153 | 154 | handleNumberKey: (number) -> 155 | if @isSearching() 156 | @searchView.getModel().insertText(number.toString()) 157 | else 158 | @activateTab(number) 159 | 160 | handleKeyEvent: (event) -> 161 | ignoredKeys = ['shift', 'control', 'alt', 'meta'] 162 | @searchView.focus() if ignoredKeys.indexOf(event.key.toLowerCase()) is -1 163 | 164 | nextTab: (n = 1) -> 165 | for tabView, i in @tabs 166 | if tabView.isActiveTab() 167 | n = @tabs.length - 1 if i+n < 0 168 | nextTabView.activateTab() if nextTabView = @tabs[(i+n)%@tabs.length] 169 | return @focus() 170 | 171 | exposeHide: -> 172 | @didIgnoreFirstChange = false 173 | for tab in @tabs 174 | tab.destroy() 175 | for panel in atom.workspace.getModalPanels() 176 | panel.hide() if panel.className is 'expose-panel' 177 | 178 | isSearching: -> @searchView.hasClass('is-focused') 179 | 180 | updateFileIcons: -> 181 | for tab in @tabs 182 | tab.updateIcon() 183 | -------------------------------------------------------------------------------- /lib/expose.coffee: -------------------------------------------------------------------------------- 1 | {CompositeDisposable, Disposable} = require 'atom' 2 | 3 | ExposeView = require './expose-view' 4 | FileIcons = require './file-icons' 5 | 6 | module.exports = Expose = 7 | exposeView: null 8 | modalPanel: null 9 | 10 | activate: -> 11 | @exposeView = new ExposeView 12 | @modalPanel = atom.workspace.addModalPanel(item: @exposeView, visible: false, className: 'expose-panel') 13 | 14 | @disposables = new CompositeDisposable 15 | 16 | @disposables.add @modalPanel.onDidChangeVisible (visible) => 17 | @exposeView.didChangeVisible(visible) 18 | 19 | @disposables.add atom.commands.add 'atom-workspace', 20 | 'expose:toggle': => @toggle() 21 | 22 | deactivate: -> 23 | @exposeView.destroy() 24 | @modalPanel.destroy() 25 | @disposables.dispose() 26 | 27 | toggle: -> 28 | if @modalPanel.isVisible() 29 | @exposeView.exposeHide() 30 | else 31 | @modalPanel.show() 32 | 33 | consumeFileIcons: (service) -> 34 | FileIcons.setService(service) 35 | @exposeView.updateFileIcons() 36 | new Disposable => 37 | FileIcons.resetService() 38 | @exposeView.updateFileIcons() 39 | -------------------------------------------------------------------------------- /lib/file-icons.coffee: -------------------------------------------------------------------------------- 1 | class FileIcons 2 | constructor: -> 3 | @service = new DefaultFileIcons 4 | 5 | getService: -> 6 | @service 7 | 8 | resetService: -> 9 | @service = new DefaultFileIcons 10 | 11 | setService: (@service) -> 12 | 13 | class DefaultFileIcons 14 | iconClassForPath: (filePath) -> 15 | '' 16 | 17 | module.exports = new FileIcons 18 | -------------------------------------------------------------------------------- /menus/expose.cson: -------------------------------------------------------------------------------- 1 | 'menu': [ 2 | { 3 | 'label': 'Packages' 4 | 'submenu': [ 5 | 'label': 'expose' 6 | 'submenu': [ 7 | { 8 | 'label': 'Toggle' 9 | 'command': 'expose:toggle' 10 | } 11 | ] 12 | ] 13 | } 14 | ] 15 | 16 | 'context-menu': 17 | '.expose-view': [ 18 | {label: 'Close Exposé', command: 'expose:close'} 19 | ] 20 | '.expose-tab': [ 21 | {label: 'Close Tab', command: 'expose:close-tab'} 22 | ] 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expose", 3 | "main": "./lib/expose", 4 | "version": "0.15.0", 5 | "description": "Quick tab overview of open files", 6 | "keywords": [ 7 | "tab", 8 | "expose", 9 | "overview", 10 | "group" 11 | ], 12 | "activationCommands": { 13 | "atom-workspace": "expose:toggle" 14 | }, 15 | "repository": "https://github.com/mrodalgaard/atom-expose", 16 | "license": "MIT", 17 | "engines": { 18 | "atom": ">=0.174.0 <2.0.0" 19 | }, 20 | "dependencies": { 21 | "atom-space-pen-views": "^2.0.3", 22 | "fuzzaldrin": "^2.1.0", 23 | "sortablejs": "^1.3.0" 24 | }, 25 | "consumedServices": { 26 | "atom.file-icons": { 27 | "versions": { 28 | "1.0.0": "consumeFileIcons" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /screenshots/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/screenshots/preview.png -------------------------------------------------------------------------------- /spec/expose-spec.coffee: -------------------------------------------------------------------------------- 1 | Expose = require '../lib/expose' 2 | 3 | describe "Expose", -> 4 | [workspaceElement, activationPromise] = [] 5 | 6 | beforeEach -> 7 | workspaceElement = atom.views.getView(atom.workspace) 8 | activationPromise = atom.packages.activatePackage('expose') 9 | 10 | describe "when the expose:toggle event is triggered", -> 11 | it "hides and shows the modal panel", -> 12 | expect(workspaceElement.querySelector('.expose-view')).not.toExist() 13 | 14 | atom.commands.dispatch workspaceElement, 'expose:toggle' 15 | 16 | waitsForPromise -> 17 | activationPromise 18 | 19 | runs -> 20 | expect(workspaceElement.querySelector('.expose-view')).toExist() 21 | 22 | exposeModule = atom.packages.loadedPackages['expose'].mainModule 23 | expect(exposeModule.modalPanel.isVisible()).toBe true 24 | atom.commands.dispatch workspaceElement, 'expose:toggle' 25 | expect(exposeModule.modalPanel.isVisible()).toBe false 26 | 27 | it "hides and shows the view", -> 28 | # This test shows you an integration test testing at the view level. 29 | 30 | # Attaching the workspaceElement to the DOM is required to allow the 31 | # `toBeVisible()` matchers to work. Anything testing visibility or focus 32 | # requires that the workspaceElement is on the DOM. Tests that attach the 33 | # workspaceElement to the DOM are generally slower than those off DOM. 34 | jasmine.attachToDOM(workspaceElement) 35 | 36 | expect(workspaceElement.querySelector('.expose-view')).not.toExist() 37 | 38 | atom.commands.dispatch workspaceElement, 'expose:toggle' 39 | 40 | waitsForPromise -> 41 | activationPromise 42 | 43 | runs -> 44 | exposeElement = workspaceElement.querySelector('.expose-view') 45 | expect(exposeElement).toBeVisible() 46 | atom.commands.dispatch workspaceElement, 'expose:toggle' 47 | expect(exposeElement).not.toBeVisible() 48 | -------------------------------------------------------------------------------- /spec/expose-tab-view-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | ExposeTabView = require '../lib/expose-tab-view' 4 | 5 | describe "ExposeTabView", -> 6 | workspaceElement = null 7 | 8 | beforeEach -> 9 | workspaceElement = atom.views.getView(atom.workspace) 10 | atom.project.setPaths [path.join(__dirname, 'fixtures')] 11 | 12 | describe "populateTabBody()", -> 13 | it "can populate empty item", -> 14 | exposeTabView = new ExposeTabView 15 | expect(Object.getOwnPropertyNames(exposeTabView.item)).toHaveLength 0 16 | expect(exposeTabView.find('.title').text()).toBe 'untitled' 17 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1 18 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'text' 19 | expect(exposeTabView.isItemPending()).toBe false 20 | 21 | it "populates normal text editor", -> 22 | waitsForPromise -> 23 | atom.workspace.open 'sample1.txt' 24 | runs -> 25 | item = atom.workspace.getActivePaneItem() 26 | exposeTabView = new ExposeTabView(item) 27 | 28 | expect(exposeTabView.item).toBeDefined() 29 | expect(exposeTabView.title).toBe 'sample1.txt' 30 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1 31 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'code' 32 | 33 | it "populates image editor", -> 34 | waitsForPromise -> 35 | atom.packages.activatePackage 'image-view' 36 | atom.workspace.open '../../screenshots/preview.png' 37 | runs -> 38 | item = atom.workspace.getActivePaneItem() 39 | exposeTabView = new ExposeTabView(item) 40 | 41 | expect(exposeTabView.item).toBeDefined() 42 | expect(exposeTabView.title).toBe 'preview.png' 43 | expect(exposeTabView.tabBody.find('img')).toHaveLength 1 44 | expect(exposeTabView.tabBody.find('img').attr('src')).toBeDefined() 45 | 46 | it "populates settings view", -> 47 | waitsForPromise -> 48 | jasmine.attachToDOM(workspaceElement) 49 | atom.packages.activatePackage 'settings-view' 50 | runs -> 51 | atom.commands.dispatch workspaceElement, 'settings-view:open' 52 | waitsFor -> 53 | atom.workspace.getActivePaneItem() 54 | runs -> 55 | item = atom.workspace.getActivePaneItem() 56 | exposeTabView = new ExposeTabView(item) 57 | 58 | expect(exposeTabView.title).toBe 'Settings' 59 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1 60 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'tools' 61 | 62 | it "populates archive view", -> 63 | waitsForPromise -> 64 | atom.packages.activatePackage 'archive-view' 65 | atom.workspace.open 'archive.zip' 66 | runs -> 67 | item = atom.workspace.getActivePaneItem() 68 | exposeTabView = new ExposeTabView(item) 69 | 70 | expect(exposeTabView.title).toBe 'archive.zip' 71 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1 72 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'zip' 73 | 74 | it "populates markdown view", -> 75 | waitsForPromise -> 76 | atom.packages.activatePackage 'markdown-preview' 77 | atom.workspace.open '../../README.md' 78 | runs -> 79 | element = atom.workspace.getActiveTextEditor().getElement() 80 | item = null 81 | atom.commands.dispatch element, 'markdown-preview:toggle' 82 | 83 | waitsFor -> 84 | item = atom.workspace.getPaneItems()[1] 85 | runs -> 86 | exposeTabView = new ExposeTabView(item) 87 | expect(exposeTabView.title).toBe 'Markdown Preview' 88 | expect(exposeTabView.tabBody.find('a')).toHaveLength 1 89 | expect(exposeTabView.tabBody.find('a').attr('class')).toContain 'markdown' 90 | 91 | it "populates text editor with minimap activated", -> 92 | waitsForPromise -> 93 | atom.packages.activatePackage 'minimap' 94 | jasmine.attachToDOM(workspaceElement) 95 | atom.workspace.open 'sample1.txt' 96 | runs -> 97 | item = atom.workspace.getActivePaneItem() 98 | exposeTabView = new ExposeTabView(item) 99 | 100 | waitsFor -> 101 | exposeTabView.tabBody.html() 102 | runs -> 103 | expect(exposeTabView.item).toBeDefined() 104 | expect(exposeTabView.title).toBe 'sample1.txt' 105 | expect(exposeTabView.tabBody.find('atom-text-editor-minimap')).toHaveLength 1 106 | 107 | it "marks pending tabs", -> 108 | waitsForPromise -> 109 | atom.workspace.open('sample1.txt', pending: true) 110 | runs -> 111 | item = atom.workspace.getActivePaneItem() 112 | exposeTabView = new ExposeTabView(item) 113 | 114 | expect(exposeTabView.title).toBe 'sample1.txt' 115 | expect(exposeTabView.isItemPending()).toBe true 116 | expect(exposeTabView.itemTitle.attr('class')).toContain 'pending' 117 | 118 | describe "closeTab()", -> 119 | it "destroys selected tab item", -> 120 | waitsForPromise -> 121 | atom.workspace.open 'sample1.txt' 122 | runs -> 123 | item = atom.workspace.getActivePaneItem() 124 | exposeTabView = new ExposeTabView(item) 125 | 126 | expect(atom.workspace.getTextEditors()).toHaveLength 1 127 | expect(exposeTabView.title).toBe 'sample1.txt' 128 | expect(exposeTabView.disposables.disposed).toBeFalsy() 129 | 130 | exposeTabView.closeTab() 131 | 132 | expect(atom.workspace.getTextEditors()).toHaveLength 0 133 | expect(exposeTabView.disposables.disposed).toBeTruthy() 134 | 135 | describe "activateTab()", -> 136 | it "activates selected tab item", -> 137 | waitsForPromise -> 138 | atom.workspace.open 'sample1.txt' 139 | atom.workspace.open 'sample2.txt' 140 | runs -> 141 | items = atom.workspace.getPaneItems() 142 | activeItem = atom.workspace.getActivePaneItem() 143 | exposeTabView = new ExposeTabView(items[0]) 144 | 145 | expect(items).toHaveLength 2 146 | expect(activeItem.getTitle()).toBe 'sample2.txt' 147 | expect(exposeTabView.title).toBe 'sample1.txt' 148 | 149 | exposeTabView.activateTab() 150 | activeItem = atom.workspace.getActivePaneItem() 151 | 152 | expect(activeItem.getTitle()).toBe 'sample1.txt' 153 | -------------------------------------------------------------------------------- /spec/expose-view-spec.coffee: -------------------------------------------------------------------------------- 1 | path = require 'path' 2 | 3 | ExposeView = require '../lib/expose-view' 4 | 5 | describe "ExposeView", -> 6 | exposeView = null 7 | 8 | beforeEach -> 9 | exposeView = new ExposeView 10 | atom.project.setPaths [path.join(__dirname, 'fixtures')] 11 | 12 | describe "update()", -> 13 | beforeEach -> 14 | waitsForPromise -> 15 | atom.workspace.open 'sample1.txt' 16 | waitsForPromise -> 17 | atom.workspace.open 'sample2.txt' 18 | 19 | it "populates list of open tabs", -> 20 | expect(exposeView.tabList.children()).toHaveLength 0 21 | expect(exposeView.tabs).toHaveLength 0 22 | exposeView.update(true) 23 | expect(exposeView.tabList.children()).toHaveLength 2 24 | expect(exposeView.tabs).toHaveLength 2 25 | 26 | it "assigns colors to different panes", -> 27 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 28 | exposeView.update(true) 29 | expect(atom.workspace.getCenter().getPanes()).toHaveLength 2 30 | expect(exposeView.tabs).toHaveLength 3 31 | 32 | color1 = exposeView.getGroupColor(0) 33 | color2 = exposeView.getGroupColor(1) 34 | expect(exposeView.tabs[1].color).toEqual color1 35 | expect(exposeView.tabs[2].color).toEqual color2 36 | expect(color1).not.toEqual color2 37 | 38 | describe "activateTab(n)", -> 39 | beforeEach -> 40 | waitsForPromise -> 41 | atom.workspace.open 'sample1.txt' 42 | waitsForPromise -> 43 | atom.workspace.open 'sample2.txt' 44 | waitsForPromise -> 45 | atom.workspace.open 'sample3.txt' 46 | 47 | it "activates given tab", -> 48 | exposeView.update(true) 49 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 50 | exposeView.activateTab(2) 51 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt' 52 | exposeView.activateTab(1) 53 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 54 | 55 | it "handles out of range input", -> 56 | exposeView.update(true) 57 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 58 | exposeView.activateTab(2) 59 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt' 60 | exposeView.activateTab(9) 61 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 62 | exposeView.activateTab(0) 63 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 64 | exposeView.activateTab() 65 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 66 | 67 | describe "moveTab(from, to)", -> 68 | beforeEach -> 69 | waitsForPromise -> 70 | atom.workspace.open 'sample1.txt' 71 | waitsForPromise -> 72 | atom.workspace.open 'sample2.txt' 73 | waitsForPromise -> 74 | atom.workspace.open 'sample3.txt' 75 | 76 | it "can move tabs", -> 77 | exposeView.update(true) 78 | expect(exposeView.tabs).toHaveLength 3 79 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt' 80 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt' 81 | exposeView.moveTab(2, 0) 82 | expect(exposeView.tabs[0].title).toEqual 'sample3.txt' 83 | expect(exposeView.tabs[2].title).toEqual 'sample2.txt' 84 | 85 | it "can move tabs between panes", -> 86 | item = atom.workspace.getActivePaneItem() 87 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 88 | item.destroy() 89 | exposeView.update(true) 90 | 91 | color1 = exposeView.getGroupColor(0) 92 | color2 = exposeView.getGroupColor(1) 93 | expect(color1).not.toEqual color2 94 | expect(exposeView.tabs).toHaveLength 3 95 | expect(exposeView.tabs[0].color).toEqual color1 96 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt' 97 | expect(exposeView.tabs[1].title).toEqual 'sample2.txt' 98 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt' 99 | expect(exposeView.tabs[2].color).toEqual color2 100 | 101 | exposeView.moveTab(0, 2) 102 | expect(exposeView.tabs[1].title).toEqual 'sample1.txt' 103 | expect(exposeView.tabs[1].color).toEqual color2 104 | expect(exposeView.tabs[0].title).toEqual 'sample2.txt' 105 | expect(exposeView.tabs[0].color).toEqual color1 106 | 107 | exposeView.moveTab(1, 0) 108 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt' 109 | expect(exposeView.tabs[0].color).toEqual color1 110 | 111 | it "uses long title when there are multiple items with the same name", -> 112 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 113 | exposeView.update(true) 114 | 115 | expect(exposeView.tabs).toHaveLength 4 116 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt' 117 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt — fixtures' 118 | expect(exposeView.tabs[3].title).toEqual 'sample3.txt — fixtures' 119 | 120 | it "handles missing long title", -> 121 | atom.workspace.getActivePane().splitRight(copyActiveItem: true) 122 | item = atom.workspace.getActivePaneItem() 123 | item.getLongTitle = undefined 124 | exposeView.update(true) 125 | 126 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt — fixtures' 127 | expect(exposeView.tabs[3].title).toEqual 'sample3.txt' 128 | 129 | it "handles invalid input", -> 130 | exposeView.update(true) 131 | exposeView.moveTab() 132 | expect(exposeView.tabs[0].title).toEqual 'sample1.txt' 133 | exposeView.moveTab(9, 9) 134 | expect(exposeView.tabs[2].title).toEqual 'sample3.txt' 135 | 136 | describe "Cycle around in tabs", -> 137 | beforeEach -> 138 | waitsForPromise -> 139 | atom.workspace.open 'sample1.txt' 140 | waitsForPromise -> 141 | atom.workspace.open 'sample2.txt' 142 | waitsForPromise -> 143 | atom.workspace.open 'sample3.txt' 144 | 145 | it "marks active tab", -> 146 | exposeView.visible = true 147 | exposeView.update() 148 | 149 | expect(exposeView.tabs[2].isActiveTab()).toBeTruthy() 150 | expect(exposeView.tabs[0].hasClass('active')).toBeFalsy() 151 | expect(exposeView.tabs[1].hasClass('active')).toBeFalsy() 152 | expect(exposeView.tabs[2].hasClass('active')).toBeTruthy() 153 | 154 | item = atom.workspace.getPaneItems()[0] 155 | atom.workspace.paneForItem(item).activateItem(item) 156 | 157 | expect(exposeView.tabs[0].isActiveTab()).toBeTruthy() 158 | expect(exposeView.tabs[2].hasClass('active')).toBeFalsy() 159 | expect(exposeView.tabs[1].hasClass('active')).toBeFalsy() 160 | expect(exposeView.tabs[0].hasClass('active')).toBeTruthy() 161 | 162 | it "can go to next tab", -> 163 | exposeView.update(true) 164 | exposeView.activateTab(1) 165 | 166 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 167 | exposeView.nextTab() 168 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt' 169 | exposeView.nextTab() 170 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 171 | exposeView.nextTab() 172 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 173 | 174 | it "can go to previous tab", -> 175 | exposeView.update(true) 176 | 177 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 178 | exposeView.nextTab(-1) 179 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample2.txt' 180 | exposeView.nextTab(-1) 181 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample1.txt' 182 | exposeView.nextTab(-1) 183 | expect(atom.workspace.getActivePaneItem().getTitle()).toEqual 'sample3.txt' 184 | 185 | describe "Hide expose view", -> 186 | [workspaceElement, activationPromise] = [] 187 | 188 | beforeEach -> 189 | workspaceElement = atom.views.getView(atom.workspace) 190 | activationPromise = atom.packages.activatePackage('expose') 191 | 192 | it "closes expose panel", -> 193 | atom.commands.dispatch workspaceElement, 'expose:toggle' 194 | 195 | waitsForPromise -> 196 | activationPromise 197 | 198 | runs -> 199 | exposeModule = atom.packages.loadedPackages['expose'].mainModule 200 | expect(exposeModule.modalPanel.isVisible()).toBe true 201 | exposeView.exposeHide() 202 | expect(exposeModule.modalPanel.isVisible()).toBe false 203 | exposeView.exposeHide() 204 | expect(exposeModule.modalPanel.isVisible()).toBe false 205 | 206 | describe "Stay updated on changes", -> 207 | beforeEach -> 208 | waitsForPromise -> 209 | atom.workspace.open 'sample1.txt' 210 | 211 | it "updates on add/destroy items", -> 212 | exposeView.visible = true 213 | exposeView.update() 214 | expect(exposeView.tabs).toHaveLength 1 215 | 216 | waitsForPromise -> 217 | atom.workspace.open 'sample2.txt' 218 | runs -> 219 | expect(exposeView.tabs).toHaveLength 2 220 | atom.workspace.getActivePaneItem().destroy() 221 | expect(exposeView.tabs).toHaveLength 1 222 | 223 | it "does not update when not visible", -> 224 | exposeView.update(true) 225 | expect(exposeView.tabs).toHaveLength 1 226 | waitsForPromise -> 227 | atom.workspace.open 'sample2.txt' 228 | runs -> 229 | expect(exposeView.tabs).toHaveLength 1 230 | 231 | describe "Filter tabs", -> 232 | beforeEach -> 233 | waitsForPromise -> 234 | atom.workspace.open 'sample1.txt' 235 | atom.workspace.open 'sample2.txt' 236 | atom.workspace.open 'sample3.txt' 237 | 238 | it "filters open tabs", -> 239 | exposeView.didChangeVisible(true) 240 | expect(exposeView.tabs).toHaveLength 3 241 | exposeView.searchView.setText '2.txt' 242 | exposeView.update(true) 243 | expect(exposeView.tabs).toHaveLength 1 244 | expect(exposeView.tabs[0].title).toBe 'sample2.txt' 245 | exposeView.searchBuffer.setText '' 246 | exposeView.update(true) 247 | expect(exposeView.tabs).toHaveLength 3 248 | -------------------------------------------------------------------------------- /spec/fixtures/archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrodalgaard/atom-expose/1056113a8fd45ef0cc3fd2419ba6a223f1f71f46/spec/fixtures/archive.zip -------------------------------------------------------------------------------- /spec/fixtures/sample1.txt: -------------------------------------------------------------------------------- 1 | sample1 2 | -------------------------------------------------------------------------------- /spec/fixtures/sample2.txt: -------------------------------------------------------------------------------- 1 | sample2 2 | -------------------------------------------------------------------------------- /spec/fixtures/sample3.txt: -------------------------------------------------------------------------------- 1 | sample3 2 | -------------------------------------------------------------------------------- /styles/expose.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | /* Layout */ 4 | 5 | atom-panel.modal.expose-panel.from-top { 6 | position: fixed; 7 | left: 0; 8 | margin: auto; 9 | top: 0; 10 | padding-top: 0; 11 | width: 100%; 12 | max-width: 100%; 13 | height: 100%; 14 | overflow: scroll; 15 | background-color: rgba(0, 0, 0, 0.4); 16 | } 17 | 18 | atom-panel.modal .expose-view { 19 | @shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.5); 20 | height: 100%; 21 | 22 | .expose-top { 23 | background-color: @base-background-color; 24 | display: flex; 25 | flex-wrap: wrap; 26 | max-width: 600px; 27 | margin: 0 auto @component-padding auto; 28 | padding: @component-padding / 2 0; 29 | 30 | .input-block-item { 31 | display: flex; 32 | padding: @component-padding / 2 @component-padding; 33 | 34 | .btn-group { 35 | display: flex; 36 | flex: 1 1 220px; 37 | .btn { 38 | display: flex; 39 | } 40 | } 41 | 42 | // Fix for invalid spacing in modal mini text editor 43 | atom-text-editor[mini] { 44 | margin-bottom: 0; 45 | .editor-contents--private { 46 | height: 22px !important; 47 | } 48 | } 49 | } 50 | 51 | .input-block-item--flex { 52 | flex: 100; 53 | position: relative; 54 | atom-text-editor { 55 | width: 100%; 56 | } 57 | } 58 | } 59 | 60 | .tab-list { 61 | display: -webkit-flex; 62 | justify-content: space-around; 63 | flex-flow: row wrap; 64 | 65 | .expose-tab { 66 | width: 210px; 67 | margin: @component-padding; 68 | -webkit-user-drag: element; 69 | border: #333 solid 1px; 70 | background: @base-background-color; 71 | 72 | .tab-header { 73 | height: 32px; 74 | padding: 8px 2px; 75 | text-align: center; 76 | cursor: move; 77 | 78 | .title { 79 | white-space: nowrap; 80 | overflow: hidden; 81 | text-overflow: ellipsis; 82 | margin: 0 15px; 83 | 84 | &.pending { 85 | font-style: italic; 86 | } 87 | 88 | &::before { 89 | margin-right: 5px; 90 | font-size: 1.1em; 91 | } 92 | } 93 | 94 | .close-icon { 95 | float: right; 96 | margin-top: -17px; 97 | cursor: pointer; 98 | transform: scale(0); 99 | transition-duration: 0.08s; 100 | } 101 | } 102 | 103 | .tab-body { 104 | position: relative; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | overflow: hidden; 109 | height: 140px; 110 | padding: 5px; 111 | border-top-style: solid; 112 | border-width: 1px; 113 | cursor: pointer; 114 | 115 | img { 116 | max-height: 100%; 117 | } 118 | 119 | a::before { 120 | .icon-resize(64px); 121 | } 122 | 123 | a.animate::before { 124 | -webkit-animation: spin 1s infinite linear; 125 | } 126 | } 127 | 128 | &:hover { 129 | .tab-body { 130 | box-shadow: @shadow; 131 | } 132 | 133 | .close-icon { 134 | transform: scale(1); 135 | transition-duration: 0.12s; 136 | } 137 | } 138 | 139 | &.active .tab-body { 140 | border-style: solid dashed dashed; 141 | padding-bottom: 4px; 142 | // Do not jump when adding a border 143 | atom-text-editor-minimap { 144 | margin-left: -1px; 145 | } 146 | } 147 | } 148 | } 149 | 150 | // Fix stand alone minimap in relative mode (issue #23) 151 | atom-text-editor-minimap[stand-alone="true"] { 152 | width: 190px; 153 | } 154 | 155 | /* Tabs numbers */ 156 | 157 | .tab-numbers-style(@content) { 158 | .close-icon { 159 | transform: none; 160 | 161 | &::before { 162 | font-family: Helvetica, Arial, sans-serif; 163 | content: @content; 164 | font-size: 12px; 165 | font-weight: bold; 166 | } 167 | } 168 | } 169 | 170 | .expose-tab:not(:hover) { 171 | &:nth-of-type(1) { 172 | .tab-numbers-style("1"); 173 | } 174 | &:nth-of-type(2) { 175 | .tab-numbers-style("2"); 176 | } 177 | &:nth-of-type(3) { 178 | .tab-numbers-style("3"); 179 | } 180 | &:nth-of-type(4) { 181 | .tab-numbers-style("4"); 182 | } 183 | &:nth-of-type(5) { 184 | .tab-numbers-style("5"); 185 | } 186 | &:nth-of-type(6) { 187 | .tab-numbers-style("6"); 188 | } 189 | &:nth-of-type(7) { 190 | .tab-numbers-style("7"); 191 | } 192 | &:nth-of-type(8) { 193 | .tab-numbers-style("8"); 194 | } 195 | &:nth-of-type(9) { 196 | .tab-numbers-style("9"); 197 | } 198 | } 199 | 200 | /* Helpers */ 201 | 202 | .ghost { 203 | opacity: 0.4; 204 | } 205 | 206 | @-webkit-keyframes spin { 207 | from { 208 | transform: rotate(0deg); 209 | } 210 | to { 211 | transform: rotate(360deg); 212 | } 213 | } 214 | 215 | .icon-resize(@size) { 216 | font-size: @size; 217 | height: @size; 218 | width: @size; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /styles/themes.less: -------------------------------------------------------------------------------- 1 | @import "ui-variables"; 2 | 3 | /* Theme adjustments */ 4 | 5 | .theme-atom-light-ui { 6 | atom-panel.modal.expose-panel { 7 | background-color: rgba(255, 255, 255, 0.4); 8 | } 9 | } 10 | 11 | .theme-one-light-ui, 12 | .theme-one-dark-ui { 13 | atom-panel.modal.expose-panel { 14 | background-color: inherit; 15 | 16 | &::before { 17 | opacity: 0; 18 | } 19 | } 20 | } 21 | 22 | .theme-atom-material-ui { 23 | atom-panel.modal.overlay.from-top { 24 | padding: 10px; 25 | } 26 | 27 | .expose-view .tab-body, 28 | .expose-view .tab-header { 29 | background-color: @app-background-color; 30 | } 31 | } 32 | 33 | .theme-native-ui { 34 | atom-panel.modal.expose-panel { 35 | top: 0 !important; 36 | } 37 | } 38 | 39 | .theme-nucleus-dark-ui { 40 | atom-panel.modal:before { 41 | background-color: inherit; 42 | } 43 | } 44 | --------------------------------------------------------------------------------