├── 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 [](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 | 
9 | 
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 |
--------------------------------------------------------------------------------