├── spec ├── fixtures │ └── dummy.txt ├── spec-helper.js ├── tab-controller-collection-spec.test.js └── atom-vim-like-tab-spec.test.js ├── .gitignore ├── keymaps └── atom-vim-like-tab.json ├── lib ├── main.js ├── tab_controller_collection.js ├── tab_panel_view.js ├── tab_controller.js ├── tab_list_view.js └── atom-vim-like-tab.js ├── .travis.yml ├── .eslintrc ├── styles └── atom-vim-like-tab.less ├── menus └── atom-vim-like-tab.json ├── LICENSE.md ├── CHANGELOG.md ├── package.json └── README.md /spec/fixtures/dummy.txt: -------------------------------------------------------------------------------- 1 | hogehoge 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /keymaps/atom-vim-like-tab.json: -------------------------------------------------------------------------------- 1 | { 2 | "atom-workspace": { 3 | } 4 | } 5 | -------------------------------------------------------------------------------- /lib/main.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import AtomVimLikeTab from './atom-vim-like-tab' 4 | 5 | export default new AtomVimLikeTab() 6 | -------------------------------------------------------------------------------- /spec/spec-helper.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import _ from 'underscore-plus' 4 | 5 | export function getTabControllers(atomVimLikeTab) { return atomVimLikeTab.tabControllers() } 6 | export function getFirstTabController(atomVimLikeTab) { return _.first(getTabControllers(atomVimLikeTab)) } 7 | export function getLastTabController(atomVimLikeTab) { return _.last(getTabControllers(atomVimLikeTab)) } 8 | export function dispatchCommand(command, element = atom.views.getView(atom.workspace)) { 9 | atom.commands.dispatch(element, command) 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: 3 | on_success: never 4 | on_failure: change 5 | 6 | script: 'curl -s https://raw.githubusercontent.com/atom/ci/master/build-package.sh | sh' 7 | 8 | git: 9 | depth: 10 10 | 11 | sudo: false 12 | 13 | os: 14 | - linux 15 | - osx 16 | 17 | dist: trusty 18 | 19 | env: 20 | global: 21 | - APM_TEST_PACKAGES="" 22 | 23 | matrix: 24 | - ATOM_CHANNEL=stable 25 | - ATOM_CHANNEL=beta 26 | 27 | addons: 28 | apt: 29 | packages: 30 | - build-essential 31 | - git 32 | - libgnome-keyring-dev 33 | - fakeroot 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017, 4 | "sourceType": "module", 5 | "ecmaFeatures": { 6 | "jsx": true 7 | } 8 | }, 9 | "plugins": [ 10 | "jasmine" 11 | ], 12 | "env": { 13 | "node": true, 14 | "jasmine": true, 15 | "atomtest": true, 16 | "browser": true, 17 | "es6": true, 18 | }, 19 | "extends": [ 20 | "eslint:recommended", 21 | "plugin:jasmine/recommended", 22 | ], 23 | "rules": { 24 | semi: ["error", "never"], 25 | }, 26 | "globals":{ 27 | "atom": false, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /styles/atom-vim-like-tab.less: -------------------------------------------------------------------------------- 1 | // The ui-variables file is provided by base themes provided by Atom. 2 | // 3 | // See https://github.com/atom/atom-dark-ui/blob/master/styles/ui-variables.less 4 | // for a full listing of what's available. 5 | @import "ui-variables"; 6 | 7 | // list view 8 | .atom-vim-like-tab { 9 | ol.list-group { 10 | max-height: 600px 11 | } 12 | .secondary-line.margin-left { 13 | margin-left: 10px 14 | } 15 | } 16 | 17 | // top panel 18 | .atom-vim-like-tab { 19 | .top-panel { 20 | background-color: #666; 21 | color: #EEE; 22 | span { 23 | padding-right: 5px; 24 | padding-left: 5px; 25 | padding-top: 1px; 26 | padding-bottom: 1px; 27 | } 28 | span.active { 29 | background-color: #444; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /menus/atom-vim-like-tab.json: -------------------------------------------------------------------------------- 1 | { 2 | "menu": [ 3 | { 4 | "label": "Packages", 5 | "submenu": [ 6 | { 7 | "label": "atom-vim-like-tab", 8 | "submenu": [ 9 | { 10 | "label": "New", 11 | "command": "atom-vim-like-tab:new" 12 | }, 13 | { 14 | "label": "Close", 15 | "command": "atom-vim-like-tab:close" 16 | }, 17 | { 18 | "label": "Next", 19 | "command": "atom-vim-like-tab:next" 20 | }, 21 | { 22 | "label": "Previous", 23 | "command": "atom-vim-like-tab:previous" 24 | }, 25 | { 26 | "label": "List", 27 | "command": "atom-vim-like-tab:list" 28 | } 29 | ] 30 | } 31 | ] 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Kesin11 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 | -------------------------------------------------------------------------------- /lib/tab_controller_collection.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { Emitter, CompositeDisposable } from 'atom' 4 | import _ from 'underscore-plus' 5 | 6 | export default class TabControllerCollection { 7 | constructor() { 8 | this.subscriptions = new CompositeDisposable() 9 | this.emitter = new Emitter() 10 | this.length = 0 11 | this.tabControllers = [] 12 | } 13 | 14 | destroy() { 15 | this.subscriptions.dispose() 16 | this.tabControllers.forEach((tabController) => { tabController.destroy() }) 17 | this.tabControllers = [] 18 | this._updateLength() 19 | } 20 | 21 | add(tabController) { 22 | this.tabControllers.push(tabController) 23 | this._updateLength() 24 | this.emitter.emit('did-change-tabControllers', this) 25 | 26 | // when close all panes destory corresponding tabController 27 | tabController.onDidPanesEmpty((tabController) => { 28 | tabController.destroy() 29 | this.remove(tabController) 30 | if (this.length > 0) this.emitter.emit('did-panes-empty') 31 | }) 32 | } 33 | 34 | remove(tabController) { 35 | _.remove(this.tabControllers, tabController) 36 | this._updateLength() 37 | this.emitter.emit('did-change-tabControllers', this) 38 | } 39 | 40 | onDidChange(callback) { 41 | this.emitter.on('did-change-tabControllers', callback) 42 | } 43 | 44 | onDidPanesEmpty(callback) { 45 | this.emitter.on('did-panes-empty', callback) 46 | } 47 | 48 | _updateLength() { 49 | this.length = this.tabControllers.length 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.5.3 - Update dependency for Security fix 2 | * [#11](https://github.com/Kesin11/atom-vim-like-tab/pull/16) Security fix 3 | 4 | ## 1.5.2 - Update dependency packages and Support Atom 1.28 5 | 6 | ## 1.5.1 - Update dependency packages 7 | * #11 8 | 9 | ## 1.5.0 - Replace test runner and update dependency modules 10 | * Replace atom default test runner to atom-mocha-test-runner 11 | * Replace atom-space-pen-view to atom-select-list 12 | 13 | ## 1.4.3 - bugfix 14 | * Bugfix 'dontRestoreInactiveTabsPane' config does not work 15 | 16 | ## 1.4.2 - Fix README 17 | * #4 Update readme to include more shortcuts 18 | 19 | ## 1.4.1 - Support Atom 1.19 20 | * Fix #3. Fix some spec for atom 1.19 breaking change Pane.close() 21 | 22 | ## 1.4.0 - Support Atom 1.17 23 | * Fix some problems by new Dock API from Atom 1.17 24 | 25 | ## 1.3.1 - Refactoring 26 | * Rewrite some view using atom/etch 27 | 28 | ## 1.3.0 - Add config 29 | * Add dontRestoreInactiveTabsPane config (default: false) 30 | 31 | ## 1.2.1 - Fix TabListView with empty panes 32 | Fixes #1 33 | Merge pull request #2 from josa42/fix-tab-list-with-empty-panes 34 | 35 | ## 1.2.0 - Add vim like tab list in top panel view 36 | * Show vim like tab list on top of editor view 37 | * Refactoring spec 38 | 39 | ## 1.1.0 - Add menu 40 | * Add each command in menu(Packages -> atom-vim-like-tab) 41 | 42 | ## 1.0.0 - Add tab list view 43 | * Add 'List' command. It shows created tabs in select-list-view. 44 | 45 | ## 0.1.0 - First Release 46 | * Implement basic feature to realize vim like tab. 47 | -------------------------------------------------------------------------------- /lib/tab_panel_view.js: -------------------------------------------------------------------------------- 1 | /** @babel */ 2 | /** @jsx etch.dom */ 3 | 4 | import etch from 'etch' 5 | 6 | export default class TabPanelView { 7 | render() { 8 | return ( 9 |
10 |
11 | {this.props.tabs.map(tab => {tab.value})} 12 |
13 |
14 | ) 15 | } 16 | 17 | constructor(tabControllerCollection) { 18 | this.tabControllerCollection = tabControllerCollection 19 | this.props = { tabs: [] } 20 | 21 | // subscriptions 22 | this.tabControllerCollection.onDidChange(() => this.update()) 23 | atom.workspace.getCenter().observeActivePaneItem(() => this.update()) 24 | 25 | etch.initialize(this) 26 | } 27 | 28 | destory() { 29 | return etch.destory(this) 30 | } 31 | 32 | update() { 33 | if (!atom.config.get('atom-vim-like-tab.enableTopTabPanel')) return 34 | 35 | this.props.tabs = this.tabControllerCollection.tabControllers.map((tabController, i) => { 36 | const item = tabController.panes[0].getActiveItem() 37 | const firstPaneName = (item) ? item.getTitle() : 'No Name' 38 | const cssClass = (tabController.isActive) ? 'active' : '' 39 | return { 40 | value: `${i + 1} ${firstPaneName}`, 41 | class: cssClass, 42 | } 43 | }) 44 | 45 | // don't show tab_panel when only one tab 46 | if (this.tabControllerCollection.length < 2) { 47 | this.props.tabs = [] 48 | } 49 | etch.update(this) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /spec/tab-controller-collection-spec.test.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { expect } from 'chai' 4 | import TabController from '../lib/tab_controller' 5 | import TabControllerCollection from '../lib/tab_controller_collection' 6 | 7 | describe('tabControllerCollection', () => { 8 | let tabControllerCollection = null 9 | beforeEach(() => { 10 | tabControllerCollection = new TabControllerCollection() 11 | }) 12 | 13 | afterEach(() => { 14 | }) 15 | 16 | it('add', () => { 17 | const tabController = new TabController() 18 | tabControllerCollection.add(tabController) 19 | 20 | expect(tabControllerCollection.tabControllers[0]).to.equal(tabController) 21 | }) 22 | 23 | it('remove', () => { 24 | const firstTabController = new TabController() 25 | const secondTabController = new TabController() 26 | tabControllerCollection.add(firstTabController) 27 | tabControllerCollection.add(secondTabController) 28 | 29 | tabControllerCollection.remove(firstTabController) 30 | 31 | expect(tabControllerCollection.tabControllers[0]).to.equal(secondTabController) 32 | 33 | expect(tabControllerCollection).to.have.lengthOf(1) 34 | }) 35 | 36 | it('length', () => { 37 | expect(tabControllerCollection).to.have.lengthOf(0) 38 | 39 | const tabController = new TabController() 40 | tabControllerCollection.add(tabController) 41 | 42 | expect(tabControllerCollection).to.have.lengthOf(1) 43 | }) 44 | 45 | it('destroy', () => { 46 | const firstTabController = new TabController() 47 | const secondTabController = new TabController() 48 | tabControllerCollection.add(firstTabController) 49 | tabControllerCollection.add(secondTabController) 50 | 51 | tabControllerCollection.destroy() 52 | 53 | expect(tabControllerCollection.tabControllers).to.have.lengthOf(0) 54 | 55 | expect(tabControllerCollection).to.have.lengthOf(0) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atom-vim-like-tab", 3 | "main": "./lib/main", 4 | "version": "1.5.3", 5 | "description": "Add Vim like tab features in Atom", 6 | "keywords": [ 7 | "Vim", 8 | "Tab", 9 | "Pane" 10 | ], 11 | "repository": "https://github.com/kesin11/atom-vim-like-tab", 12 | "license": "MIT", 13 | "engines": { 14 | "atom": ">=1.28.0 <2.0.0" 15 | }, 16 | "scripts": { 17 | "lint": "./node_modules/.bin/eslint lib spec", 18 | "test": "atom --test ./spec/*", 19 | "test:beta": "atom --test ./spec/*", 20 | "watch": "./node_modules/.bin/npm-watch test", 21 | "watch:beta": "./node_modules/.bin/npm-watch test:beta" 22 | }, 23 | "devDependencies": { 24 | "atom-mocha-test-runner": "1.2.0", 25 | "chai": "4.1.2", 26 | "eslint": "^4.19.1", 27 | "eslint-config-airbnb-base": "^13.0.0", 28 | "eslint-plugin-import": "2.13.0", 29 | "eslint-plugin-jasmine": "^2.9.3", 30 | "npm-watch": "^0.3.0" 31 | }, 32 | "dependencies": { 33 | "atom-select-list": "0.7.1", 34 | "etch": "^0.14.0", 35 | "underscore-plus": "^1.6.6" 36 | }, 37 | "configSchema": { 38 | "enableTopTabPanel": { 39 | "type": "boolean", 40 | "default": true, 41 | "description": "If enable, vim like tab list will shown on top of editor view." 42 | }, 43 | "dontRestoreInactiveTabsPane": { 44 | "type": "boolean", 45 | "default": false, 46 | "description": "At startup, prevents restoring the tabs that inactived at the time of last quit. ## NOTE: This function using atom private API. Maybe not working newer atom version ##" 47 | } 48 | }, 49 | "watch": { 50 | "test": { 51 | "patterns": [ 52 | "lib", 53 | "spec" 54 | ], 55 | "extensions": "js", 56 | "quiet": true 57 | }, 58 | "test:beta": { 59 | "patterns": [ 60 | "lib", 61 | "spec" 62 | ], 63 | "extensions": "js", 64 | "quiet": true 65 | } 66 | }, 67 | "atomTestRunner": "atom-mocha-test-runner" 68 | } 69 | -------------------------------------------------------------------------------- /lib/tab_controller.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { Emitter, CompositeDisposable } from 'atom' 4 | import _ from 'underscore-plus' 5 | 6 | export default class TabController { 7 | constructor(panes = []) { 8 | this.subscriptions = new CompositeDisposable 9 | this.emitter = new Emitter 10 | this.panes = [] 11 | this.activate() 12 | 13 | // add subscriptions for init panes 14 | panes.forEach((pane) => { 15 | this.panes.push(pane) 16 | this.subscriptions.add(this.paneSubscriptions(pane)) 17 | }) 18 | 19 | this.subscriptions.add( 20 | atom.workspace.getCenter().onDidAddPane((event) => { 21 | if (!this.isActive) return 22 | 23 | this.panes.push(event.pane) 24 | this.subscriptions.add(this.paneSubscriptions(event.pane)) 25 | }) 26 | ) 27 | } 28 | 29 | destroy() { 30 | this.subscriptions.dispose() 31 | } 32 | 33 | paneSubscriptions(pane) { 34 | const paneSubscriptions = new CompositeDisposable() 35 | 36 | paneSubscriptions.add( 37 | pane.onDidDestroy(() => { 38 | _.remove(this.panes, pane) 39 | paneSubscriptions.dispose() 40 | 41 | if (_.isEmpty(this.panes)) this.emitter.emit('did-pane-empty', this) 42 | }) 43 | ) 44 | return paneSubscriptions 45 | } 46 | 47 | show() { 48 | this.panes.forEach((pane) => { 49 | const view = atom.views.getView(pane) 50 | view.style.display = '' 51 | }) 52 | this.activate() 53 | if (!_.isUndefined(this.panes[0])) { 54 | this.panes[0].activate() 55 | } 56 | } 57 | 58 | hide() { 59 | this.panes.forEach((pane) => { 60 | const view = atom.views.getView(pane) 61 | view.style.display = 'none' 62 | }) 63 | this.deactivate() 64 | } 65 | 66 | activate() { 67 | this.isActive = true 68 | } 69 | 70 | deactivate() { 71 | this.isActive = false 72 | } 73 | 74 | closeAllPanes() { 75 | // copy panes because pane.close() will remove pane from this.panes. 76 | // forEach will broken when removing iterate object. 77 | const panes = this.panes.concat() 78 | panes.forEach(async (pane) => await pane.close()) 79 | } 80 | 81 | onDidPanesEmpty(callback) { 82 | this.emitter.on('did-pane-empty', callback) 83 | } 84 | 85 | getPaneViews() { 86 | return this.panes.map((pane) => atom.views.getView(pane)) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/tab_list_view.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import { Emitter } from 'atom' 4 | import SelectListView from 'atom-select-list' 5 | import _ from 'underscore-plus' 6 | 7 | export default class TabListView { 8 | constructor(tabControllers = []) { 9 | this.emitter = new Emitter 10 | this.tabControllers = tabControllers 11 | this.panel = null 12 | this.selectListView = new SelectListView({ 13 | items: [], 14 | filterKeyForItem: (item) => item.primaryTitle, 15 | didConfirmSelection: (item) => { 16 | this.selectListView.reset() 17 | this.emitter.emit('did-tab-list-confirmed', item.index) 18 | }, 19 | didCancelSelection: () => { 20 | this.selectListView.reset() 21 | this.panel.hide() 22 | }, 23 | elementForItem: ({ primaryTitle, secondaryTitles }) => { 24 | const li = document.createElement('li') 25 | li.classList.add('two-lines') 26 | 27 | const span = document.createElement('span') 28 | span.textContent = primaryTitle 29 | span.classList.add('primary-line') 30 | li.appendChild(span) 31 | 32 | secondaryTitles.forEach((title, i) => { 33 | const div = document.createElement('div') 34 | div.textContent = `${i + 1}: ${title}` 35 | div.classList.add('secondary-line', 'margin-left') 36 | span.appendChild(div) 37 | }) 38 | return li 39 | }, 40 | }) 41 | this.selectListView.element.classList.add('atom-vim-like-tab') 42 | } 43 | show() { 44 | this.selectListView.update({items: this.createItems()}) 45 | 46 | if (!this.panel) { 47 | this.panel = atom.workspace.addModalPanel({item: this.selectListView}) 48 | } 49 | this.panel.show() 50 | 51 | this.selectListView.focus() 52 | } 53 | createItems() { 54 | return this.tabControllers.map((tabController, i) => { 55 | const activeItem = tabController.panes[0].getActiveItem() 56 | const primaryTitle = `${i + 1} ${activeItem ? activeItem.getTitle() : 'No name'}` 57 | const paneItems = tabController.panes.map((pane) => pane.getItems()) 58 | const secondaryTitles = _.flatten(paneItems).map((item) => item.getTitle()) 59 | return { primaryTitle, secondaryTitles, index: i } 60 | }) 61 | } 62 | 63 | onDidTabListConfirmed(callback) { 64 | this.emitter.on('did-tab-list-confirmed', callback) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atom-vim-like-tab package [![Build Status](https://travis-ci.org/Kesin11/atom-vim-like-tab.svg?branch=master)](https://travis-ci.org/Kesin11/atom-vim-like-tab) 2 | 3 | Add Vim like tab features in Atom 4 | 5 | Create virtual window that can have multiple pane. 6 | It emulate vim tab features. 7 | 8 | ![atom-vim-like-tab.gif](https://raw.githubusercontent.com/Kesin11/atom-vim-like-tab/images/images/atom-vim-like-tab1_2.gif) 9 | ![tab_list_view.png](https://raw.githubusercontent.com/Kesin11/atom-vim-like-tab/images/images/tab_list_view.png) 10 | 11 | # Commands 12 | - `atom-vim-like-tab:new`: crate new tab 13 | - `atom-vim-like-tab:close`: close current tab 14 | - `atom-vim-like-tab:previous`: show previous tab 15 | - `atom-vim-like-tab:next`: show next tab 16 | - `atom-vim-like-tab:list`: open tab select panel 17 | 18 | # Keymap 19 | 20 | No default keymaps. 21 | Here is my example 22 | 23 | ``` 24 | '.editor.vim-mode-plus:not(.insert-mode)': 25 | 't c': 'atom-vim-like-tab:new' # mean 'tab create' 26 | ': t a b c': 'atom-vim-like-tab:close' 27 | 't p': 'atom-vim-like-tab:previous' 28 | 't n': 'atom-vim-like-tab:next' 29 | 'space t': 'atom-vim-like-tab:list' 30 | ``` 31 | 32 | If you're using [ex-mode](https://atom.io/packages/ex-mode) here are a few additional shortcuts to be more like Real Vim (plus, it should be easy to see how to add your own!) 33 | ``` 34 | // keymap.cson 35 | '.editor.vim-mode-plus:not(.insert-mode)': 36 | 'g t': 'atom-vim-like-tab:next' 37 | 'g T': 'atom-vim-like-tab:previous' 38 | ``` 39 | ``` 40 | // init.coffee 41 | atom.packages.onDidActivatePackage (pack) -> 42 | if pack.name == 'ex-mode' 43 | Ex = pack.mainModule.provideEx() 44 | Ex.registerCommand 'tabs', -> 45 | atomWorkspace = atom.views.getView(atom.workspace) 46 | setTimeout -> 47 | atom.commands.dispatch(atomWorkspace, 'atom-vim-like-tab:list') 48 | , 0 49 | Ex.registerCommand 'tab', -> 50 | atomWorkspace = atom.views.getView(atom.workspace) 51 | setTimeout -> 52 | atom.commands.dispatch(atomWorkspace, 'atom-vim-like-tab:new') 53 | , 0 54 | Ex.registerCommand 'tabn', -> 55 | atomWorkspace = atom.views.getView(atom.workspace) 56 | setTimeout -> 57 | atom.commands.dispatch(atomWorkspace, 'atom-vim-like-tab:next') 58 | , 0 59 | Ex.registerCommand 'tabp', -> 60 | atomWorkspace = atom.views.getView(atom.workspace) 61 | setTimeout -> 62 | atom.commands.dispatch(atomWorkspace, 'atom-vim-like-tab:previous') 63 | , 0 64 | Ex.registerCommand 'tabclose', -> 65 | atomWorkspace = atom.views.getView(atom.workspace) 66 | setTimeout -> 67 | atom.commands.dispatch(atomWorkspace, 'atom-vim-like-tab:close') 68 | , 0 69 | ``` 70 | 71 | # Future work 72 | - [x] Add packages menu 73 | - [x] Add list view feature for show and select tab 74 | - [x] Always show how many tab and which is current tab. inspire by vim 75 | 76 | # License 77 | MIT 78 | -------------------------------------------------------------------------------- /lib/atom-vim-like-tab.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import TabController from './tab_controller' 4 | import TabControllerCollection from './tab_controller_collection' 5 | import TabListView from './tab_list_view' 6 | import TabPanelView from './tab_panel_view' 7 | import { CompositeDisposable } from 'atom' 8 | 9 | export default class AtomVimLikeTab { 10 | constructor () { 11 | this.tabControllerCollection = null 12 | this.tabListView = null 13 | this.tabPanelView = null 14 | this.subscriptions = null 15 | this.showIndex = 0 16 | } 17 | 18 | async activate() { 19 | this.subscriptions = new CompositeDisposable() 20 | this.tabControllerCollection = new TabControllerCollection() 21 | 22 | this.tabPanelView = new TabPanelView(this.tabControllerCollection) 23 | this.tabPanel = atom.workspace.addTopPanel({ item: this.tabPanelView.element }) 24 | 25 | const initPanes = atom.workspace.getCenter().getPanes() 26 | this.tabControllerCollection.add(new TabController(initPanes)) 27 | 28 | this.subscriptions.add(atom.commands.add('atom-workspace', { 29 | 'atom-vim-like-tab:new': () => this.createNewTab(), 30 | 'atom-vim-like-tab:close': () => this.closeTab(), 31 | 'atom-vim-like-tab:next': () => this.showNextTab(), 32 | 'atom-vim-like-tab:previous': () => this.showPreviousTab(), 33 | 'atom-vim-like-tab:list': () => this.createTabListView().show(), 34 | })) 35 | 36 | this.tabControllerCollection.onDidPanesEmpty(() => this.showPreviousTab()) 37 | } 38 | 39 | async deactivate() { 40 | let promise = Promise.resolve() 41 | if (atom.config.get('atom-vim-like-tab.dontRestoreInactiveTabsPane')) { 42 | this.closeInactiveTabs() 43 | // NOTE: saveState() is not public API. Maybe not working newer atom version. 44 | atom.saveState() 45 | 46 | // hack for waiting async pane.close() and atom.saveState() 47 | promise = new Promise((resolve) => setTimeout(resolve, 1000)) 48 | } 49 | 50 | this.subscriptions.dispose() 51 | this.tabControllerCollection.destroy() 52 | this.tabListView = null 53 | 54 | return promise 55 | } 56 | 57 | tabControllers() { 58 | return this.tabControllerCollection.tabControllers 59 | } 60 | 61 | createNewTab() { 62 | // hide current tab 63 | this.tabControllers().forEach((tabController) => tabController.hide()) 64 | 65 | // create new pane for new tab and change active pane 66 | const tabController = new TabController 67 | const activePane = atom.workspace.getCenter().getActivePane() 68 | activePane.splitRight() 69 | 70 | this.tabControllerCollection.add(tabController) 71 | this.showIndex = this.tabControllerCollection.length - 1 72 | } 73 | 74 | closeTab() { 75 | const closingTabController = this.tabControllers().find(tabController => 76 | tabController.isActive === true 77 | ) 78 | closingTabController.closeAllPanes() 79 | } 80 | 81 | closeInactiveTabs() { 82 | this.tabControllers() 83 | .filter(tabController => !tabController.isActive) 84 | .forEach(tabController => tabController.closeAllPanes()) 85 | } 86 | 87 | showTabByIndex(index) { 88 | this.showIndex = this.getValidShowIndex(index) 89 | this.tabControllers().forEach((tabController, i) => { 90 | if (i !== this.showIndex) tabController.hide() 91 | }) 92 | this.tabControllers()[this.showIndex].show() 93 | } 94 | 95 | showNextTab() { 96 | this.showTabByIndex(this.showIndex + 1) 97 | } 98 | 99 | showPreviousTab() { 100 | this.showTabByIndex(this.showIndex - 1) 101 | } 102 | 103 | getValidShowIndex(index) { 104 | if (index >= this.tabControllerCollection.length) { 105 | return 0 106 | } else if (index < 0) { 107 | return this.tabControllerCollection.length - 1 108 | } 109 | 110 | return index 111 | } 112 | 113 | createTabListView() { 114 | if (this.tabListView === null) { 115 | this.tabListView = new TabListView(this.tabControllers()) 116 | // when tabListView confirmed show selected tab 117 | this.tabListView.onDidTabListConfirmed((index) => { 118 | this.showTabByIndex(index) 119 | }) 120 | } 121 | return this.tabListView 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /spec/atom-vim-like-tab-spec.test.js: -------------------------------------------------------------------------------- 1 | 'use babel' 2 | 3 | import AtomVimLikeTab from '../lib/atom-vim-like-tab' 4 | import { expect } from 'chai' 5 | import * as path from 'path' 6 | import { 7 | getTabControllers, 8 | getFirstTabController, 9 | getLastTabController, 10 | dispatchCommand, 11 | } from './spec-helper.js' 12 | import TabController from '../lib/tab_controller' 13 | import _ from 'underscore-plus' 14 | 15 | describe('AtomVimLikeTab', () => { 16 | let atomVimLikeTab 17 | beforeEach(async () => { 18 | atomVimLikeTab = new AtomVimLikeTab() 19 | await atomVimLikeTab.activate() 20 | await atom.workspace.open(path.join(__dirname, 'fixtures', 'dummy.txt')) 21 | }) 22 | 23 | afterEach(() => { 24 | atomVimLikeTab.deactivate() 25 | }) 26 | 27 | describe('activation', () => { 28 | describe('when activated', () => { 29 | it('has one tabContoller', () => { 30 | expect(getTabControllers(atomVimLikeTab)).to.have.lengthOf(1) 31 | 32 | expect(getFirstTabController(atomVimLikeTab)).to.be.instanceOf(TabController) 33 | }) 34 | }) 35 | 36 | describe('when deactivated', () => { 37 | beforeEach(() => { 38 | atomVimLikeTab.deactivate() 39 | }) 40 | 41 | it('subscriptions should be disposed', () => { 42 | expect(atomVimLikeTab.subscriptions.disposed).to.be.true 43 | }) 44 | 45 | it('tabControllers should be empty', () => { 46 | expect(getTabControllers(atomVimLikeTab)).to.have.lengthOf(0) 47 | }) 48 | }) 49 | 50 | describe('when deactivate with config "dontRestoreInactiveTabsPane"', () => { 51 | let inactivePaneIds = undefined 52 | let activePaneId = undefined 53 | 54 | beforeEach(async () => { 55 | atom.config.set('atom-vim-like-tab.dontRestoreInactiveTabsPane', true) 56 | dispatchCommand('atom-vim-like-tab:new') 57 | dispatchCommand('atom-vim-like-tab:new') 58 | dispatchCommand('atom-vim-like-tab:new') 59 | inactivePaneIds = getTabControllers(atomVimLikeTab) 60 | .filter(tabController => !tabController.isActive) 61 | .map(tabController => tabController.panes[0].id) 62 | activePaneId = getLastTabController(atomVimLikeTab).panes[0].id 63 | 64 | atomVimLikeTab.deactivate() 65 | 66 | // hack for pane.close() called by atom-vim-like-tab:close completly done. 67 | // because dispatchCommand() can't return promise. 68 | await new Promise(resolve => setTimeout(resolve, 100)) 69 | }) 70 | 71 | it('inactive tabs pane should be closed', () => { 72 | const paneIds = atom.workspace.getCenter().getPanes().map(pane => pane.id) 73 | 74 | expect(paneIds).to.contain(activePaneId) 75 | 76 | for (const inactivePaneId of inactivePaneIds) { 77 | expect(paneIds).to.not.contain(inactivePaneId) 78 | } 79 | }) 80 | }) 81 | }) 82 | 83 | describe('dispatch command', () => { 84 | describe('new', () => { 85 | 86 | it('tabControllers should be have new controller', () => { 87 | const beforeControllersNum = getTabControllers(atomVimLikeTab).length 88 | dispatchCommand('atom-vim-like-tab:new') 89 | 90 | expect(getTabControllers(atomVimLikeTab)).to.have.lengthOf.above(beforeControllersNum) 91 | }) 92 | 93 | it('old pane should be hide', () => { 94 | dispatchCommand('atom-vim-like-tab:new') 95 | 96 | const oldController = getFirstTabController(atomVimLikeTab) 97 | 98 | expect( 99 | oldController.getPaneViews().every( 100 | (view) => view.style.display === 'none' 101 | )).to.be.true 102 | }) 103 | 104 | it('new tabControllers should be have another pane', () => { 105 | const beforePanes = getFirstTabController(atomVimLikeTab).panes 106 | dispatchCommand('atom-vim-like-tab:new') 107 | 108 | const newPane = _.first(getLastTabController(atomVimLikeTab).panes) 109 | 110 | expect(beforePanes).to.not.contain(newPane) 111 | }) 112 | 113 | it('new pane should be managed new tabContoller after new command', () => { 114 | dispatchCommand('atom-vim-like-tab:new') 115 | atom.workspace.getCenter().getActivePane().splitRight() 116 | const newPane = atom.workspace.getCenter().getActivePane() 117 | 118 | const oldControllerPanes = getFirstTabController(atomVimLikeTab).panes 119 | const newControllerPanes = getLastTabController(atomVimLikeTab).panes 120 | 121 | expect(oldControllerPanes).to.not.contain(newPane) 122 | 123 | expect(newControllerPanes).to.contain(newPane) 124 | }) 125 | }) 126 | 127 | describe('next', () => { 128 | beforeEach(() => { 129 | dispatchCommand('atom-vim-like-tab:new') 130 | }) 131 | 132 | it('current tab should be hide', () => { 133 | const beforeShowIndex = atomVimLikeTab.showIndex 134 | dispatchCommand('atom-vim-like-tab:next') 135 | 136 | const previousController = getTabControllers(atomVimLikeTab)[beforeShowIndex] 137 | 138 | expect( 139 | previousController.getPaneViews().every( 140 | (view) => view.style.display === 'none' 141 | )).to.be.true 142 | }) 143 | 144 | it('next tab should be show', () => { 145 | dispatchCommand('atom-vim-like-tab:next') 146 | const showIndex = atomVimLikeTab.showIndex 147 | const nextController = getTabControllers(atomVimLikeTab)[showIndex] 148 | 149 | expect(nextController.getPaneViews().every( 150 | (view) => view.style.display === '' 151 | )).to.be.true 152 | }) 153 | 154 | it('next tab pane should be activated', () => { 155 | dispatchCommand('atom-vim-like-tab:next') 156 | const showIndex = atomVimLikeTab.showIndex 157 | const nextController = getTabControllers(atomVimLikeTab)[showIndex] 158 | 159 | expect(nextController.panes[0]).to.be.eql(atom.workspace.getCenter().getActivePane()) 160 | }) 161 | }) 162 | 163 | describe('close', () => { 164 | describe('when before create new tab', () => { 165 | 166 | it('last TabController should not be removed', async () => { 167 | const initController = getFirstTabController(atomVimLikeTab) 168 | dispatchCommand('atom-vim-like-tab:close') 169 | // hack for pane.close() called by atom-vim-like-tab:close completly done. 170 | // because dispatchCommand() can't return promise. 171 | await new Promise(resolve => setTimeout(resolve, 100)) 172 | 173 | expect(initController.panes).to.have.lengthOf(1) 174 | 175 | expect(getTabControllers(atomVimLikeTab)).to.contain(initController) 176 | }) 177 | }) 178 | 179 | describe('when after carete new tab', () => { 180 | let currentController = null 181 | beforeEach(async () => { 182 | dispatchCommand('atom-vim-like-tab:new') 183 | currentController = getLastTabController(atomVimLikeTab) 184 | 185 | dispatchCommand('atom-vim-like-tab:close') 186 | // hack for pane.close() called by atom-vim-like-tab:close completly done. 187 | // because dispatchCommand() can't return promise. 188 | await new Promise(resolve => setTimeout(resolve, 100)) 189 | }) 190 | 191 | it('current TabController should be removed', () => { 192 | 193 | expect(currentController.panes).to.have.lengthOf(0) 194 | 195 | expect(getTabControllers(atomVimLikeTab)).not.to.contain(currentController) 196 | }) 197 | 198 | it('previous panes should be show', () => { 199 | const showIndex = atomVimLikeTab.showIndex 200 | const previousController = getTabControllers(atomVimLikeTab)[showIndex] 201 | 202 | expect(previousController.getPaneViews().every( 203 | (view) => view.style.display === '' 204 | )).to.be.true 205 | }) 206 | 207 | it('previous tab pane should be activated', () => { 208 | const showIndex = atomVimLikeTab.showIndex 209 | const previousController = getTabControllers(atomVimLikeTab)[showIndex] 210 | 211 | expect(previousController.panes[0]).to.be.eql(atom.workspace.getCenter().getActivePane()) 212 | }) 213 | }) 214 | }) 215 | 216 | describe('triggered by outside action', () => { 217 | describe('when all panes are closed', () => { 218 | it('unnecessary tabController should be removed', async () => { 219 | // create new tab and then close all pane 220 | dispatchCommand('atom-vim-like-tab:new') 221 | const newController = getLastTabController(atomVimLikeTab) 222 | const closePromises = newController.panes.map((pane) => pane.close()) 223 | 224 | await Promise.all(closePromises) 225 | 226 | expect(getTabControllers(atomVimLikeTab)).not.to.contain(newController) 227 | }) 228 | }) 229 | }) 230 | }) 231 | }) 232 | --------------------------------------------------------------------------------