diff --git a/.coffeelintignore b/.coffeelintignore deleted file mode 100644 index 1db51fe..0000000 --- a/.coffeelintignore +++ /dev/null @@ -1 +0,0 @@ -spec/fixtures diff --git a/coffeelint.json b/coffeelint.json deleted file mode 100644 index a5dd715..0000000 --- a/coffeelint.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "max_line_length": { - "level": "ignore" - }, - "no_empty_param_list": { - "level": "error" - }, - "arrow_spacing": { - "level": "error" - }, - "no_interpolation_in_single_quotes": { - "level": "error" - }, - "no_debugger": { - "level": "error" - }, - "prefer_english_operator": { - "level": "error" - }, - "colon_assignment_spacing": { - "spacing": { - "left": 0, - "right": 1 - }, - "level": "error" - }, - "braces_spacing": { - "spaces": 0, - "level": "error" - }, - "spacing_after_comma": { - "level": "error" - }, - "no_stand_alone_at": { - "level": "error" - } -} diff --git a/lib/cursor-position-view.coffee b/lib/cursor-position-view.coffee deleted file mode 100644 index 847edd2..0000000 --- a/lib/cursor-position-view.coffee +++ /dev/null @@ -1,63 +0,0 @@ -{Disposable} = require 'atom' - -module.exports = -class CursorPositionView - constructor: -> - @viewUpdatePending = false - - @element = document.createElement('status-bar-cursor') - @element.classList.add('cursor-position', 'inline-block') - @goToLineLink = document.createElement('a') - @goToLineLink.classList.add('inline-block') - @element.appendChild(@goToLineLink) - - @formatString = atom.config.get('status-bar.cursorPositionFormat') ? '%L:%C' - - @activeItemSubscription = atom.workspace.onDidChangeActiveTextEditor (activeEditor) => @subscribeToActiveTextEditor() - - @subscribeToConfig() - @subscribeToActiveTextEditor() - - @tooltip = atom.tooltips.add(@element, title: => "Line #{@row}, Column #{@column}") - - @handleClick() - - destroy: -> - @activeItemSubscription.dispose() - @cursorSubscription?.dispose() - @tooltip.dispose() - @configSubscription?.dispose() - @clickSubscription.dispose() - @updateSubscription?.dispose() - - subscribeToActiveTextEditor: -> - @cursorSubscription?.dispose() - selectionsMarkerLayer = atom.workspace.getActiveTextEditor()?.selectionsMarkerLayer - @cursorSubscription = selectionsMarkerLayer?.onDidUpdate(@scheduleUpdate.bind(this)) - @scheduleUpdate() - - subscribeToConfig: -> - @configSubscription?.dispose() - @configSubscription = atom.config.observe 'status-bar.cursorPositionFormat', (value) => - @formatString = value ? '%L:%C' - @scheduleUpdate() - - handleClick: -> - clickHandler = -> atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), 'go-to-line:toggle') - @element.addEventListener('click', clickHandler) - @clickSubscription = new Disposable => @element.removeEventListener('click', clickHandler) - - scheduleUpdate: -> - return if @viewUpdatePending - - @viewUpdatePending = true - @updateSubscription = atom.views.updateDocument => - @viewUpdatePending = false - if position = atom.workspace.getActiveTextEditor()?.getCursorBufferPosition() - @row = position.row + 1 - @column = position.column + 1 - @goToLineLink.textContent = @formatString.replace('%L', @row).replace('%C', @column) - @element.classList.remove('hide') - else - @goToLineLink.textContent = '' - @element.classList.add('hide') diff --git a/lib/cursor-position-view.js b/lib/cursor-position-view.js new file mode 100644 index 0000000..90fb75b --- /dev/null +++ b/lib/cursor-position-view.js @@ -0,0 +1,74 @@ +const {Disposable} = require('atom') + +module.exports = +class CursorPositionView { + constructor () { + this.viewUpdatePending = false + + this.element = document.createElement('status-bar-cursor') + this.element.classList.add('cursor-position', 'inline-block') + this.goToLineLink = document.createElement('a') + this.goToLineLink.classList.add('inline-block') + this.element.appendChild(this.goToLineLink) + + this.formatString = atom.config.get('status-bar.cursorPositionFormat') || '%L:%C' + + this.activeItemSubscription = atom.workspace.onDidChangeActiveTextEditor(activeEditor => this.subscribeToActiveTextEditor()) + + this.subscribeToConfig() + this.subscribeToActiveTextEditor() + + this.tooltip = atom.tooltips.add(this.element, {title: () => `Line ${this.row}, Column ${this.column}`}) + + this.handleClick() + } + + destroy () { + this.activeItemSubscription.dispose() + if (this.cursorSubscription) this.cursorSubscription.dispose() + this.tooltip.dispose() + if (this.configSubscription) this.configSubscription.dispose() + this.clickSubscription.dispose() + if (this.updateSubscription) this.updateSubscription.dispose() + } + + subscribeToActiveTextEditor () { + if (this.cursorSubscription) this.cursorSubscription.dispose() + const selectionsMarkerLayer = atom.workspace.getActiveTextEditor() && atom.workspace.getActiveTextEditor().selectionsMarkerLayer + this.cursorSubscription = selectionsMarkerLayer && selectionsMarkerLayer.onDidUpdate(this.scheduleUpdate.bind(this)) + this.scheduleUpdate() + } + + subscribeToConfig () { + if (this.configSubscription) this.configSubscription.dispose() + this.configSubscription = atom.config.observe('status-bar.cursorPositionFormat', value => { + this.formatString = value || '%L:%C' + this.scheduleUpdate() + }) + } + + handleClick () { + const clickHandler = () => atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), 'go-to-line:toggle') + this.element.addEventListener('click', clickHandler) + this.clickSubscription = new Disposable(() => this.element.removeEventListener('click', clickHandler)) + } + + scheduleUpdate () { + if (this.viewUpdatePending) return + + this.viewUpdatePending = true + this.updateSubscription = atom.views.updateDocument(() => { + this.viewUpdatePending = false + const position = atom.workspace.getActiveTextEditor() && atom.workspace.getActiveTextEditor().getCursorBufferPosition() + if (position) { + this.row = position.row + 1 + this.column = position.column + 1 + this.goToLineLink.textContent = this.formatString.replace('%L', this.row).replace('%C', this.column) + this.element.classList.remove('hide') + } else { + this.goToLineLink.textContent = '' + this.element.classList.add('hide') + } + }) + } +} diff --git a/lib/file-info-view.coffee b/lib/file-info-view.coffee deleted file mode 100644 index 6c62555..0000000 --- a/lib/file-info-view.coffee +++ /dev/null @@ -1,118 +0,0 @@ -{Disposable} = require 'atom' -url = require 'url' -fs = require 'fs-plus' - -module.exports = -class FileInfoView - constructor: -> - @element = document.createElement('status-bar-file') - @element.classList.add('file-info', 'inline-block') - - @currentPath = document.createElement('a') - @currentPath.classList.add('current-path') - @element.appendChild(@currentPath) - @element.currentPath = @currentPath - - @element.getActiveItem = @getActiveItem.bind(this) - - @activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem => - @subscribeToActiveItem() - @subscribeToActiveItem() - - @registerTooltip() - clickHandler = (event) => - isShiftClick = event.shiftKey - @showCopiedTooltip(isShiftClick) - text = @getActiveItemCopyText(isShiftClick) - atom.clipboard.write(text) - setTimeout => - @clearCopiedTooltip() - , 2000 - - @currentPath.addEventListener('click', clickHandler) - @clickSubscription = new Disposable => @currentPath.removeEventListener('click', clickHandler) - - registerTooltip: -> - @tooltip = atom.tooltips.add(@element, title: -> - "Click to copy absolute file path (Shift + Click to copy relative path)") - - clearCopiedTooltip: -> - @copiedTooltip?.dispose() - @registerTooltip() - - showCopiedTooltip: (copyRelativePath) -> - @tooltip?.dispose() - @copiedTooltip?.dispose() - text = @getActiveItemCopyText(copyRelativePath) - @copiedTooltip = atom.tooltips.add @element, - title: "Copied: #{text}" - trigger: 'click' - delay: - show: 0 - - getActiveItemCopyText: (copyRelativePath) -> - activeItem = @getActiveItem() - path = activeItem?.getPath?() - return activeItem?.getTitle?() or '' if not path? - - # Make sure we try to relativize before parsing URLs. - if copyRelativePath - relativized = atom.project.relativize(path) - if relativized isnt path - return relativized - - # An item path could be a url, we only want to copy the `path` part - if path?.indexOf('://') > 0 - path = url.parse(path).path - path - - subscribeToActiveItem: -> - @modifiedSubscription?.dispose() - @titleSubscription?.dispose() - - if activeItem = @getActiveItem() - @updateCallback ?= => @update() - - if typeof activeItem.onDidChangeTitle is 'function' - @titleSubscription = activeItem.onDidChangeTitle(@updateCallback) - else if typeof activeItem.on is 'function' - #TODO Remove once title-changed event support is removed - activeItem.on('title-changed', @updateCallback) - @titleSubscription = dispose: => - activeItem.off?('title-changed', @updateCallback) - - @modifiedSubscription = activeItem.onDidChangeModified?(@updateCallback) - - @update() - - destroy: -> - @activeItemSubscription.dispose() - @titleSubscription?.dispose() - @modifiedSubscription?.dispose() - @clickSubscription?.dispose() - @copiedTooltip?.dispose() - @tooltip?.dispose() - - getActiveItem: -> - atom.workspace.getCenter().getActivePaneItem() - - update: -> - @updatePathText() - @updateBufferHasModifiedText(@getActiveItem()?.isModified?()) - - updateBufferHasModifiedText: (isModified) -> - if isModified - @element.classList.add('buffer-modified') - @isModified = true - else - @element.classList.remove('buffer-modified') - @isModified = false - - updatePathText: -> - if path = @getActiveItem()?.getPath?() - relativized = atom.project.relativize(path) - @currentPath.textContent = if relativized? then fs.tildify(relativized) else path - else if title = @getActiveItem()?.getTitle?() - @currentPath.textContent = title - else - @currentPath.textContent = '' diff --git a/lib/file-info-view.js b/lib/file-info-view.js new file mode 100644 index 0000000..2309e18 --- /dev/null +++ b/lib/file-info-view.js @@ -0,0 +1,146 @@ +const {Disposable} = require('atom') +const url = require('url') +const fs = require('fs-plus') + +module.exports = +class FileInfoView { + constructor () { + this.element = document.createElement('status-bar-file') + this.element.classList.add('file-info', 'inline-block') + + this.currentPath = document.createElement('a') + this.currentPath.classList.add('current-path') + this.element.appendChild(this.currentPath) + this.element.currentPath = this.currentPath + + this.element.getActiveItem = this.getActiveItem.bind(this) + + this.activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem(() => this.subscribeToActiveItem()) + this.subscribeToActiveItem() + + this.registerTooltip() + const clickHandler = event => { + const isShiftClick = event.shiftKey + this.showCopiedTooltip(isShiftClick) + const text = this.getActiveItemCopyText(isShiftClick) + atom.clipboard.write(text) + setTimeout(() => this.clearCopiedTooltip(), 2000) + } + + this.currentPath.addEventListener('click', clickHandler) + this.clickSubscription = new Disposable(() => this.currentPath.removeEventListener('click', clickHandler)) + } + + registerTooltip () { + this.tooltip = atom.tooltips.add(this.element, { title () { + 'Click to copy absolute file path (Shift + Click to copy relative path)' + } + }) + } + + clearCopiedTooltip () { + if (this.copiedTooltip) this.copiedTooltip.dispose() + this.registerTooltip() + } + + showCopiedTooltip (copyRelativePath) { + if (this.tooltip) this.tooltip.dispose() + if (this.copiedTooltip) this.copiedTooltip.dispose() + const text = this.getActiveItemCopyText(copyRelativePath) + this.copiedTooltip = atom.tooltips.add(this.element, { + title: `Copied: ${text}`, + trigger: 'click', + delay: { + show: 0 + } + }) + } + + getActiveItemCopyText (copyRelativePath) { + const activeItem = this.getActiveItem() + let path = activeItem && activeItem.getPath() + if (path == null) { + const activeTitle = activeItem && activeItem.getTitle() + return activeTitle || '' + } + + // Make sure we try to relativize before parsing URLs. + if (copyRelativePath) { + const relativized = atom.project.relativize(path) + if (relativized !== path) return relativized + } + + // An item path could be a url, we only want to copy the `path` part + if (path && path.indexOf('://') > 0) { + ({path} = url.parse(path)) + } + return path + } + + subscribeToActiveItem () { + if (this.modifiedSubscription) this.modifiedSubscription.dispose() + if (this.titleSubscription) this.titleSubscription.dispose() + + const activeItem = this.getActiveItem() + if (activeItem) { + if (this.updateCallback == null) this.updateCallback = () => this.update() + + if (typeof activeItem.onDidChangeTitle === 'function') { + this.titleSubscription = activeItem.onDidChangeTitle(this.updateCallback) + } else if (typeof activeItem.on === 'function') { + // TODO Remove once title-changed event support is removed + activeItem.on('title-changed', this.updateCallback) + this.titleSubscription = { dispose: () => { + typeof activeItem.off === 'function' && activeItem.off('title-changed', this.updateCallback) + } + } + } + + this.modifiedSubscription = typeof activeItem.onDidChangeModified === 'function' && activeItem.onDidChangeModified(this.updateCallback) + } + + this.update() + } + + destroy () { + this.activeItemSubscription.dispose() + if (this.titleSubscription) this.titleSubscription.dispose() + if (this.modifiedSubscription) this.modifiedSubscription.dispose() + if (this.clickSubscription) this.clickSubscription.dispose() + if (this.copiedTooltip) this.copiedTooltip.dispose() + if (this.tooltip) this.tooltip.dispose() + } + + getActiveItem () { + return atom.workspace.getCenter().getActivePaneItem() + } + + update () { + this.updatePathText() + this.updateBufferHasModifiedText(this.getActiveItem() && typeof this.getActiveItem().isModified === 'function' && this.getActiveItem().isModified()) + } + + updateBufferHasModifiedText (isModified) { + if (isModified) { + this.element.classList.add('buffer-modified') + this.isModified = true + } else { + this.element.classList.remove('buffer-modified') + this.isModified = false + } + } + + updatePathText () { + const activeItem = this.getActiveItem() + const path = activeItem && typeof activeItem.getPath === 'function' && activeItem.getPath() + const title = activeItem && typeof activeItem.getTitle === 'function' && activeItem.getTitle() + if (path) { + const relativized = atom.project.relativize(path) + this.currentPath.textContent = relativized ? fs.tildify(relativized) : path + } else if (title) { + this.currentPath.textContent = title + } else { + this.currentPath.textContent = '' + } + } +} diff --git a/lib/git-view.coffee b/lib/git-view.coffee deleted file mode 100644 index 624ab17..0000000 --- a/lib/git-view.coffee +++ /dev/null @@ -1,222 +0,0 @@ -_ = require "underscore-plus" -{CompositeDisposable, GitRepositoryAsync} = require "atom" - -module.exports = -class GitView - constructor: -> - @element = document.createElement('status-bar-git') - @element.classList.add('git-view') - - @createBranchArea() - @createCommitsArea() - @createStatusArea() - - @activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem => - @subscribeToActiveItem() - @projectPathSubscription = atom.project.onDidChangePaths => - @subscribeToRepositories() - @subscribeToRepositories() - @subscribeToActiveItem() - - createBranchArea: -> - @branchArea = document.createElement('div') - @branchArea.classList.add('git-branch', 'inline-block') - @element.appendChild(@branchArea) - @element.branchArea = @branchArea - - branchIcon = document.createElement('span') - branchIcon.classList.add('icon', 'icon-git-branch') - @branchArea.appendChild(branchIcon) - - @branchLabel = document.createElement('span') - @branchLabel.classList.add('branch-label') - @branchArea.appendChild(@branchLabel) - @element.branchLabel = @branchLabel - - createCommitsArea: -> - @commitsArea = document.createElement('div') - @commitsArea.classList.add('git-commits', 'inline-block') - @element.appendChild(@commitsArea) - - @commitsAhead = document.createElement('span') - @commitsAhead.classList.add('icon', 'icon-arrow-up', 'commits-ahead-label') - @commitsArea.appendChild(@commitsAhead) - - @commitsBehind = document.createElement('span') - @commitsBehind.classList.add('icon', 'icon-arrow-down', 'commits-behind-label') - @commitsArea.appendChild(@commitsBehind) - - createStatusArea: -> - @gitStatus = document.createElement('div') - @gitStatus.classList.add('git-status', 'inline-block') - @element.appendChild(@gitStatus) - - @gitStatusIcon = document.createElement('span') - @gitStatusIcon.classList.add('icon') - @gitStatus.appendChild(@gitStatusIcon) - @element.gitStatusIcon = @gitStatusIcon - - subscribeToActiveItem: -> - activeItem = @getActiveItem() - - @savedSubscription?.dispose() - @savedSubscription = activeItem?.onDidSave? => @update() - - @update() - - subscribeToRepositories: -> - @repositorySubscriptions?.dispose() - @repositorySubscriptions = new CompositeDisposable - - for repo in atom.project.getRepositories() when repo? - @repositorySubscriptions.add repo.onDidChangeStatus ({path, status}) => - @update() if path is @getActiveItemPath() - @repositorySubscriptions.add repo.onDidChangeStatuses => - @update() - - destroy: -> - @activeItemSubscription?.dispose() - @projectPathSubscription?.dispose() - @savedSubscription?.dispose() - @repositorySubscriptions?.dispose() - @branchTooltipDisposable?.dispose() - @commitsAheadTooltipDisposable?.dispose() - @commitsBehindTooltipDisposable?.dispose() - @statusTooltipDisposable?.dispose() - - getActiveItemPath: -> - @getActiveItem()?.getPath?() - - getRepositoryForActiveItem: -> - [rootDir] = atom.project.relativizePath(@getActiveItemPath()) - rootDirIndex = atom.project.getPaths().indexOf(rootDir) - if rootDirIndex >= 0 - atom.project.getRepositories()[rootDirIndex] - else - for repo in atom.project.getRepositories() when repo - return repo - - getActiveItem: -> - atom.workspace.getCenter().getActivePaneItem() - - update: -> - repo = @getRepositoryForActiveItem() - @updateBranchText(repo) - @updateAheadBehindCount(repo) - @updateStatusText(repo) - - updateBranchText: (repo) -> - if @showGitInformation(repo) - head = repo.getShortHead(@getActiveItemPath()) - @branchLabel.textContent = head - @branchArea.style.display = '' if head - @branchTooltipDisposable?.dispose() - @branchTooltipDisposable = atom.tooltips.add @branchArea, title: "On branch #{head}" - else - @branchArea.style.display = 'none' - - showGitInformation: (repo) -> - return false unless repo? - - if itemPath = @getActiveItemPath() - atom.project.contains(itemPath) - else - not @getActiveItem()? - - updateAheadBehindCount: (repo) -> - unless @showGitInformation(repo) - @commitsArea.style.display = 'none' - return - - itemPath = @getActiveItemPath() - {ahead, behind} = repo.getCachedUpstreamAheadBehindCount(itemPath) - if ahead > 0 - @commitsAhead.textContent = ahead - @commitsAhead.style.display = '' - @commitsAheadTooltipDisposable?.dispose() - @commitsAheadTooltipDisposable = atom.tooltips.add @commitsAhead, title: "#{_.pluralize(ahead, 'commit')} ahead of upstream" - else - @commitsAhead.style.display = 'none' - - if behind > 0 - @commitsBehind.textContent = behind - @commitsBehind.style.display = '' - @commitsBehindTooltipDisposable?.dispose() - @commitsBehindTooltipDisposable = atom.tooltips.add @commitsBehind, title: "#{_.pluralize(behind, 'commit')} behind upstream" - else - @commitsBehind.style.display = 'none' - - if ahead > 0 or behind > 0 - @commitsArea.style.display = '' - else - @commitsArea.style.display = 'none' - - clearStatus: -> - @gitStatusIcon.classList.remove('icon-diff-modified', 'status-modified', 'icon-diff-added', 'status-added', 'icon-diff-ignored', 'status-ignored') - - updateAsNewFile: -> - @clearStatus() - - @gitStatusIcon.classList.add('icon-diff-added', 'status-added') - if textEditor = atom.workspace.getActiveTextEditor() - @gitStatusIcon.textContent = "+#{textEditor.getLineCount()}" - @updateTooltipText("#{_.pluralize(textEditor.getLineCount(), 'line')} in this new file not yet committed") - else - @gitStatusIcon.textContent = '' - @updateTooltipText() - - @gitStatus.style.display = '' - - updateAsModifiedFile: (repo, path) -> - stats = repo.getDiffStats(path) - @clearStatus() - - @gitStatusIcon.classList.add('icon-diff-modified', 'status-modified') - if stats.added and stats.deleted - @gitStatusIcon.textContent = "+#{stats.added}, -#{stats.deleted}" - @updateTooltipText("#{_.pluralize(stats.added, 'line')} added and #{_.pluralize(stats.deleted, 'line')} deleted in this file not yet committed") - else if stats.added - @gitStatusIcon.textContent = "+#{stats.added}" - @updateTooltipText("#{_.pluralize(stats.added, 'line')} added to this file not yet committed") - else if stats.deleted - @gitStatusIcon.textContent = "-#{stats.deleted}" - @updateTooltipText("#{_.pluralize(stats.deleted, 'line')} deleted from this file not yet committed") - else - @gitStatusIcon.textContent = '' - @updateTooltipText() - - @gitStatus.style.display = '' - - updateAsIgnoredFile: -> - @clearStatus() - - @gitStatusIcon.classList.add('icon-diff-ignored', 'status-ignored') - @gitStatusIcon.textContent = '' - @gitStatus.style.display = '' - @updateTooltipText("File is ignored by git") - - updateTooltipText: (text) -> - @statusTooltipDisposable?.dispose() - if text - @statusTooltipDisposable = atom.tooltips.add @gitStatusIcon, title: text - - updateStatusText: (repo) -> - hideStatus = => - @clearStatus() - @gitStatus.style.display = 'none' - - itemPath = @getActiveItemPath() - if @showGitInformation(repo) and itemPath? - status = repo.getCachedPathStatus(itemPath) ? 0 - if repo.isStatusNew(status) - return @updateAsNewFile() - - if repo.isStatusModified(status) - return @updateAsModifiedFile(repo, itemPath) - - if repo.isPathIgnored(itemPath) - @updateAsIgnoredFile() - else - hideStatus() - else - hideStatus() diff --git a/lib/git-view.js b/lib/git-view.js new file mode 100644 index 0000000..f4f6b25 --- /dev/null +++ b/lib/git-view.js @@ -0,0 +1,259 @@ +const _ = require('underscore-plus') +const {CompositeDisposable} = require('atom') + +module.exports = +class GitView { + constructor () { + this.element = document.createElement('status-bar-git') + this.element.classList.add('git-view') + + this.createBranchArea() + this.createCommitsArea() + this.createStatusArea() + + this.activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem(() => this.subscribeToActiveItem()) + this.projectPathSubscription = atom.project.onDidChangePaths(() => this.subscribeToRepositories()) + this.subscribeToRepositories() + this.subscribeToActiveItem() + } + + createBranchArea () { + this.branchArea = document.createElement('div') + this.branchArea.classList.add('git-branch', 'inline-block') + this.element.appendChild(this.branchArea) + this.element.branchArea = this.branchArea + + const branchIcon = document.createElement('span') + branchIcon.classList.add('icon', 'icon-git-branch') + this.branchArea.appendChild(branchIcon) + + this.branchLabel = document.createElement('span') + this.branchLabel.classList.add('branch-label') + this.branchArea.appendChild(this.branchLabel) + this.element.branchLabel = this.branchLabel + } + + createCommitsArea () { + this.commitsArea = document.createElement('div') + this.commitsArea.classList.add('git-commits', 'inline-block') + this.element.appendChild(this.commitsArea) + + this.commitsAhead = document.createElement('span') + this.commitsAhead.classList.add('icon', 'icon-arrow-up', 'commits-ahead-label') + this.commitsArea.appendChild(this.commitsAhead) + + this.commitsBehind = document.createElement('span') + this.commitsBehind.classList.add('icon', 'icon-arrow-down', 'commits-behind-label') + this.commitsArea.appendChild(this.commitsBehind) + } + + createStatusArea () { + this.gitStatus = document.createElement('div') + this.gitStatus.classList.add('git-status', 'inline-block') + this.element.appendChild(this.gitStatus) + + this.gitStatusIcon = document.createElement('span') + this.gitStatusIcon.classList.add('icon') + this.gitStatus.appendChild(this.gitStatusIcon) + this.element.gitStatusIcon = this.gitStatusIcon + } + + subscribeToActiveItem () { + const activeItem = this.getActiveItem() + + if (this.savedSubscription) this.savedSubscription.dispose() + this.savedSubscription = activeItem && typeof activeItem.onDidSave === 'function' && activeItem.onDidSave(() => this.update()) + + this.update() + } + + subscribeToRepositories () { + if (this.repositorySubscriptions) this.repositorySubscriptions.dispose() + this.repositorySubscriptions = new CompositeDisposable() + + for (let repo of atom.project.getRepositories()) { + if (repo) { + this.repositorySubscriptions.add(repo.onDidChangeStatus(({path, status}) => { + if (path === this.getActiveItemPath()) this.update() + })) + this.repositorySubscriptions.add(repo.onDidChangeStatuses(() => this.update())) + } + } + } + + destroy () { + if (this.activeItemSubscription) this.activeItemSubscription.dispose() + if (this.projectPathSubscription) this.projectPathSubscription.dispose() + if (this.savedSubscription) this.savedSubscription.dispose() + if (this.repositorySubscriptions) this.repositorySubscriptions.dispose() + if (this.branchTooltipDisposable) this.branchTooltipDisposable.dispose() + if (this.commitsAheadTooltipDisposable) this.commitsAheadTooltipDisposable.dispose() + if (this.commitsBehindTooltipDisposable) this.commitsBehindTooltipDisposable.dispose() + if (this.statusTooltipDisposable) this.statusTooltipDisposable.dispose() + } + + getActiveItemPath () { + const activeItem = this.getActiveItem() + return activeItem && typeof activeItem.getPath === 'function' && activeItem.getPath() + } + + getRepositoryForActiveItem () { + const [rootDir] = atom.project.relativizePath(this.getActiveItemPath()) + const rootDirIndex = atom.project.getPaths().indexOf(rootDir) + if (rootDirIndex >= 0) { + return atom.project.getRepositories()[rootDirIndex] + } else { + for (let repo of atom.project.getRepositories()) { + if (repo) return repo + } + } + } + + getActiveItem () { + return atom.workspace.getCenter().getActivePaneItem() + } + + update () { + const repo = this.getRepositoryForActiveItem() + this.updateBranchText(repo) + this.updateAheadBehindCount(repo) + this.updateStatusText(repo) + } + + updateBranchText (repo) { + if (this.showGitInformation(repo)) { + const head = repo.getShortHead(this.getActiveItemPath()) + this.branchLabel.textContent = head + if (head) this.branchArea.style.display = '' + if (this.branchTooltipDisposable) { + this.branchTooltipDisposable.dispose() + } + this.branchTooltipDisposable = atom.tooltips.add(this.branchArea, {title: `On branch ${head}`}) + } else { + this.branchArea.style.display = 'none' + } + } + + showGitInformation (repo) { + if (repo == null) return false + + const itemPath = this.getActiveItemPath() + if (itemPath) { + return atom.project.contains(itemPath) + } else { + return this.getActiveItem() == null + } + } + + updateAheadBehindCount (repo) { + if (!this.showGitInformation(repo)) { + this.commitsArea.style.display = 'none' + return + } + + const itemPath = this.getActiveItemPath() + const {ahead, behind} = repo.getCachedUpstreamAheadBehindCount(itemPath) + if (ahead > 0) { + this.commitsAhead.textContent = ahead + this.commitsAhead.style.display = '' + if (this.commitsAheadTooltipDisposable) this.commitsAheadTooltipDisposable.dispose() + this.commitsAheadTooltipDisposable = atom.tooltips.add(this.commitsAhead, {title: `${_.pluralize(ahead, 'commit')} ahead of upstream`}) + } else { + this.commitsAhead.style.display = 'none' + } + + if (behind > 0) { + this.commitsBehind.textContent = behind + this.commitsBehind.style.display = '' + if (this.commitsBehindTooltipDisposable) this.commitsBehindTooltipDisposable.dispose() + this.commitsBehindTooltipDisposable = atom.tooltips.add(this.commitsBehind, {title: `${_.pluralize(behind, 'commit')} behind upstream`}) + } else { + this.commitsBehind.style.display = 'none' + } + + if (ahead > 0 || behind > 0) { + this.commitsArea.style.display = '' + } else { + this.commitsArea.style.display = 'none' + } + } + + clearStatus () { + this.gitStatusIcon.classList.remove('icon-diff-modified', 'status-modified', 'icon-diff-added', 'status-added', 'icon-diff-ignored', 'status-ignored') + } + + updateAsNewFile () { + this.clearStatus() + + this.gitStatusIcon.classList.add('icon-diff-added', 'status-added') + const textEditor = atom.workspace.getActiveTextEditor() + if (textEditor) { + this.gitStatusIcon.textContent = `+${textEditor.getLineCount()}` + this.updateTooltipText(`${_.pluralize(textEditor.getLineCount(), 'line')} in this new file not yet committed`) + } else { + this.gitStatusIcon.textContent = '' + this.updateTooltipText() + } + + this.gitStatus.style.display = '' + } + + updateAsModifiedFile (repo, path) { + const stats = repo.getDiffStats(path) + this.clearStatus() + + this.gitStatusIcon.classList.add('icon-diff-modified', 'status-modified') + if (stats.added && stats.deleted) { + this.gitStatusIcon.textContent = `+${stats.added}, -${stats.deleted}` + this.updateTooltipText(`${_.pluralize(stats.added, 'line')} added and ${_.pluralize(stats.deleted, 'line')} deleted in this file not yet committed`) + } else if (stats.added) { + this.gitStatusIcon.textContent = `+${stats.added}` + this.updateTooltipText(`${_.pluralize(stats.added, 'line')} added to this file not yet committed`) + } else if (stats.deleted) { + this.gitStatusIcon.textContent = `-${stats.deleted}` + this.updateTooltipText(`${_.pluralize(stats.deleted, 'line')} deleted from this file not yet committed`) + } else { + this.gitStatusIcon.textContent = '' + this.updateTooltipText() + } + + this.gitStatus.style.display = '' + } + + updateAsIgnoredFile () { + this.clearStatus() + + this.gitStatusIcon.classList.add('icon-diff-ignored', 'status-ignored') + this.gitStatusIcon.textContent = '' + this.gitStatus.style.display = '' + this.updateTooltipText('File is ignored by git') + } + + updateTooltipText (text) { + if (this.statusTooltipDisposable) this.statusTooltipDisposable.dispose() + if (text) this.statusTooltipDisposable = atom.tooltips.add(this.gitStatusIcon, {title: text}) + } + + updateStatusText (repo) { + const hideStatus = () => { + this.clearStatus() + this.gitStatus.style.display = 'none' + } + + const itemPath = this.getActiveItemPath() + if (this.showGitInformation(repo) && itemPath) { + const status = repo.getCachedPathStatus(itemPath) || 0 + if (repo.isStatusNew(status)) { + this.updateAsNewFile() + } else if (repo.isStatusModified(status)) { + this.updateAsModifiedFile(repo, itemPath) + } else if (repo.isPathIgnored(itemPath)) { + this.updateAsIgnoredFile() + } else { + hideStatus() + } + } else { + hideStatus() + } + } +} diff --git a/lib/launch-mode-view.coffee b/lib/launch-mode-view.coffee deleted file mode 100644 index 2611ca4..0000000 --- a/lib/launch-mode-view.coffee +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = -class LaunchModeView - constructor: ({safeMode, devMode}={}) -> - @element = document.createElement('status-bar-launch-mode') - @element.classList.add('inline-block', 'icon', 'icon-color-mode') - if devMode - @element.classList.add('text-error') - @tooltipDisposable = atom.tooltips.add(@element, title: 'This window is in dev mode') - else if safeMode - @element.classList.add('text-success') - @tooltipDisposable = atom.tooltips.add(@element, title: 'This window is in safe mode') - - detachedCallback: -> - @tooltipDisposable?.dispose() diff --git a/lib/launch-mode-view.js b/lib/launch-mode-view.js new file mode 100644 index 0000000..3890f54 --- /dev/null +++ b/lib/launch-mode-view.js @@ -0,0 +1,19 @@ +module.exports = +class LaunchModeView { + constructor (param) { + const {safeMode, devMode} = param + this.element = document.createElement('status-bar-launch-mode') + this.element.classList.add('inline-block', 'icon', 'icon-color-mode') + if (devMode) { + this.element.classList.add('text-error') + this.tooltipDisposable = atom.tooltips.add(this.element, {title: 'This window is in dev mode'}) + } else if (safeMode) { + this.element.classList.add('text-success') + this.tooltipDisposable = atom.tooltips.add(this.element, {title: 'This window is in safe mode'}) + } + } + + detachedCallback () { + this.tooltipDisposable.dispose() + } +} diff --git a/lib/main.coffee b/lib/main.coffee deleted file mode 100644 index fecdd20..0000000 --- a/lib/main.coffee +++ /dev/null @@ -1,121 +0,0 @@ -{CompositeDisposable, Emitter} = require 'atom' -Grim = require 'grim' -StatusBarView = require './status-bar-view' -FileInfoView = require './file-info-view' -CursorPositionView = require './cursor-position-view' -SelectionCountView = require './selection-count-view' -GitView = require './git-view' -LaunchModeView = require './launch-mode-view' - -module.exports = - activate: -> - @emitters = new Emitter() - @subscriptions = new CompositeDisposable() - - @statusBar = new StatusBarView() - @attachStatusBar() - - @subscriptions.add atom.config.onDidChange 'status-bar.fullWidth', => - @attachStatusBar() - - @updateStatusBarVisibility() - - @statusBarVisibilitySubscription = - atom.config.observe 'status-bar.isVisible', => - @updateStatusBarVisibility() - - atom.commands.add 'atom-workspace', 'status-bar:toggle', => - if @statusBarPanel.isVisible() - atom.config.set 'status-bar.isVisible', false - else - atom.config.set 'status-bar.isVisible', true - - {safeMode, devMode} = atom.getLoadSettings() - if safeMode or devMode - launchModeView = new LaunchModeView({safeMode, devMode}) - @statusBar.addLeftTile(item: launchModeView.element, priority: -1) - - @fileInfo = new FileInfoView() - @statusBar.addLeftTile(item: @fileInfo.element, priority: 0) - - @cursorPosition = new CursorPositionView() - @statusBar.addLeftTile(item: @cursorPosition.element, priority: 1) - - @selectionCount = new SelectionCountView() - @statusBar.addLeftTile(item: @selectionCount.element, priority: 2) - - @gitInfo = new GitView() - @gitInfoTile = @statusBar.addRightTile(item: @gitInfo.element, priority: 0) - - deactivate: -> - @statusBarVisibilitySubscription?.dispose() - @statusBarVisibilitySubscription = null - - @gitInfo?.destroy() - @gitInfo = null - - @fileInfo?.destroy() - @fileInfo = null - - @cursorPosition?.destroy() - @cursorPosition = null - - @selectionCount?.destroy() - @selectionCount = null - - @statusBarPanel?.destroy() - @statusBarPanel = null - - @statusBar?.destroy() - @statusBar = null - - @subscriptions?.dispose() - @subscriptions = null - - @emitters?.dispose() - @emitters = null - - delete atom.__workspaceView.statusBar if atom.__workspaceView? - - updateStatusBarVisibility: -> - if atom.config.get 'status-bar.isVisible' - @statusBarPanel.show() - else - @statusBarPanel.hide() - - provideStatusBar: -> - addLeftTile: @statusBar.addLeftTile.bind(@statusBar) - addRightTile: @statusBar.addRightTile.bind(@statusBar) - getLeftTiles: @statusBar.getLeftTiles.bind(@statusBar) - getRightTiles: @statusBar.getRightTiles.bind(@statusBar) - disableGitInfoTile: @gitInfoTile.destroy.bind(@gitInfoTile) - - attachStatusBar: -> - @statusBarPanel.destroy() if @statusBarPanel? - - panelArgs = {item: @statusBar, priority: 0} - if atom.config.get('status-bar.fullWidth') - @statusBarPanel = atom.workspace.addFooterPanel panelArgs - else - @statusBarPanel = atom.workspace.addBottomPanel panelArgs - - # Deprecated - # - # Wrap deprecation calls on the methods returned rather than - # Services API method which would be registered and trigger - # a deprecation call - legacyProvideStatusBar: -> - statusbar = @provideStatusBar() - - addLeftTile: (args...) -> - Grim.deprecate("Use version ^1.0.0 of the status-bar Service API.") - statusbar.addLeftTile(args...) - addRightTile: (args...) -> - Grim.deprecate("Use version ^1.0.0 of the status-bar Service API.") - statusbar.addRightTile(args...) - getLeftTiles: -> - Grim.deprecate("Use version ^1.0.0 of the status-bar Service API.") - statusbar.getLeftTiles() - getRightTiles: -> - Grim.deprecate("Use version ^1.0.0 of the status-bar Service API.") - statusbar.getRightTiles() diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 0000000..7084b98 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,136 @@ +const {CompositeDisposable, Emitter} = require('atom') +const Grim = require('grim') +const StatusBarView = require('./status-bar-view') +const FileInfoView = require('./file-info-view') +const CursorPositionView = require('./cursor-position-view') +const SelectionCountView = require('./selection-count-view') +const GitView = require('./git-view') +const LaunchModeView = require('./launch-mode-view') + +module.exports = { + activate () { + this.emitters = new Emitter() + this.subscriptions = new CompositeDisposable() + + this.statusBar = new StatusBarView() + this.attachStatusBar() + + this.subscriptions.add(atom.config.onDidChange('status-bar.fullWidth', () => this.attachStatusBar())) + + this.updateStatusBarVisibility() + + this.statusBarVisibilitySubscription = atom.config.observe('status-bar.isVisible', () => this.updateStatusBarVisibility()) + + atom.commands.add('atom-workspace', 'status-bar:toggle', () => { + if (this.statusBarPanel.isVisible()) { + atom.config.set('status-bar.isVisible', false) + } else { + atom.config.set('status-bar.isVisible', true) + } + }) + + const {safeMode, devMode} = atom.getLoadSettings() + if (safeMode || devMode) { + const launchModeView = new LaunchModeView({safeMode, devMode}) + this.statusBar.addLeftTile({item: launchModeView.element, priority: -1}) + } + + this.fileInfo = new FileInfoView() + this.statusBar.addLeftTile({item: this.fileInfo.element, priority: 0}) + + this.cursorPosition = new CursorPositionView() + this.statusBar.addLeftTile({item: this.cursorPosition.element, priority: 1}) + + this.selectionCount = new SelectionCountView() + this.statusBar.addLeftTile({item: this.selectionCount.element, priority: 2}) + + this.gitInfo = new GitView() + this.gitInfoTile = this.statusBar.addRightTile({item: this.gitInfo.element, priority: 0}) + }, + + deactivate () { + if (this.statusBarVisibilitySubscription) this.statusBarVisibilitySubscription.dispose() + this.statusBarVisibilitySubscription = null + + if (this.gitInfo) this.gitInfo.destroy() + this.gitInfo = null + + if (this.fileInfo) this.fileInfo.destroy() + this.fileInfo = null + + if (this.cursorPosition) this.cursorPosition.destroy() + this.cursorPosition = null + + if (this.selectionCount) this.selectionCount.destroy() + this.selectionCount = null + + if (this.statusBarPanel) this.statusBarPanel.destroy() + this.statusBarPanel = null + + if (this.statusBar) this.statusBar.destroy() + this.statusBar = null + + if (this.subscriptions) this.subscriptions.dispose() + this.subscriptions = null + + if (this.emitters) this.emitters.dispose() + this.emitters = null + }, + + updateStatusBarVisibility () { + if (atom.config.get('status-bar.isVisible')) { + this.statusBarPanel.show() + } else { + this.statusBarPanel.hide() + } + }, + + provideStatusBar () { + return { + addLeftTile: this.statusBar.addLeftTile.bind(this.statusBar), + addRightTile: this.statusBar.addRightTile.bind(this.statusBar), + getLeftTiles: this.statusBar.getLeftTiles.bind(this.statusBar), + getRightTiles: this.statusBar.getRightTiles.bind(this.statusBar), + disableGitInfoTile: this.gitInfoTile.destroy.bind(this.gitInfoTile) + } + }, + + attachStatusBar () { + if (this.statusBarPanel) this.statusBarPanel.destroy() + + const panelArgs = {item: this.statusBar, priority: 0} + if (atom.config.get('status-bar.fullWidth')) { + this.statusBarPanel = atom.workspace.addFooterPanel(panelArgs) + } else { + this.statusBarPanel = atom.workspace.addBottomPanel(panelArgs) + } + }, + + // Deprecated + // + // Wrap deprecation calls on the methods returned rather than + // Services API method which would be registered and trigger + // a deprecation call + legacyProvideStatusBar () { + const statusbar = this.provideStatusBar() + + return { + addLeftTile (...args) { + Grim.deprecate('Use version ^1.0.0 of the status-bar Service API.') + statusbar.addLeftTile(...args) + }, + addRightTile (...args) { + Grim.deprecate('Use version ^1.0.0 of the status-bar Service API.') + statusbar.addRightTile(...args) + }, + getLeftTiles () { + Grim.deprecate('Use version ^1.0.0 of the status-bar Service API.') + return statusbar.getLeftTiles() + }, + getRightTiles () { + Grim.deprecate('Use version ^1.0.0 of the status-bar Service API.') + return statusbar.getRightTiles() + } + } + } +} diff --git a/lib/selection-count-view.coffee b/lib/selection-count-view.coffee deleted file mode 100644 index 35bb9f6..0000000 --- a/lib/selection-count-view.coffee +++ /dev/null @@ -1,58 +0,0 @@ -_ = require 'underscore-plus' - -module.exports = -class SelectionCountView - constructor: -> - @element = document.createElement('status-bar-selection') - @element.classList.add('selection-count', 'inline-block') - - @tooltipElement = document.createElement('div') - @tooltipDisposable = atom.tooltips.add @element, item: @tooltipElement - - @formatString = atom.config.get('status-bar.selectionCountFormat') ? '(%L, %C)' - - @activeItemSubscription = atom.workspace.onDidChangeActiveTextEditor => @subscribeToActiveTextEditor() - - @subscribeToConfig() - @subscribeToActiveTextEditor() - - destroy: -> - @activeItemSubscription.dispose() - @selectionSubscription?.dispose() - @configSubscription?.dispose() - @tooltipDisposable.dispose() - - subscribeToConfig: -> - @configSubscription?.dispose() - @configSubscription = atom.config.observe 'status-bar.selectionCountFormat', (value) => - @formatString = value ? '(%L, %C)' - @scheduleUpdateCount() - - subscribeToActiveTextEditor: -> - @selectionSubscription?.dispose() - activeEditor = @getActiveTextEditor() - selectionsMarkerLayer = activeEditor?.selectionsMarkerLayer - @selectionSubscription = selectionsMarkerLayer?.onDidUpdate(@scheduleUpdateCount.bind(this)) - @scheduleUpdateCount() - - getActiveTextEditor: -> - atom.workspace.getActiveTextEditor() - - scheduleUpdateCount: -> - unless @scheduledUpdate - @scheduledUpdate = true - atom.views.updateDocument => - @updateCount() - @scheduledUpdate = false - - updateCount: -> - count = @getActiveTextEditor()?.getSelectedText().length - range = @getActiveTextEditor()?.getSelectedBufferRange() - lineCount = range?.getRowCount() - lineCount -= 1 if range?.end.column is 0 - if count > 0 - @element.textContent = @formatString.replace('%L', lineCount).replace('%C', count) - @tooltipElement.textContent = "#{_.pluralize(lineCount, 'line')}, #{_.pluralize(count, 'character')} selected" - else - @element.textContent = '' - @tooltipElement.textContent = '' diff --git a/lib/selection-count-view.js b/lib/selection-count-view.js new file mode 100644 index 0000000..3f57039 --- /dev/null +++ b/lib/selection-count-view.js @@ -0,0 +1,70 @@ +const _ = require('underscore-plus') + +module.exports = +class SelectionCountView { + constructor () { + this.element = document.createElement('status-bar-selection') + this.element.classList.add('selection-count', 'inline-block') + + this.tooltipElement = document.createElement('div') + this.tooltipDisposable = atom.tooltips.add(this.element, {item: this.tooltipElement}) + + this.formatString = atom.config.get('status-bar.selectionCountFormat') || '(%L, %C)' + + this.activeItemSubscription = atom.workspace.onDidChangeActiveTextEditor(() => this.subscribeToActiveTextEditor()) + + this.subscribeToConfig() + this.subscribeToActiveTextEditor() + } + + destroy () { + this.activeItemSubscription.dispose() + if (this.selectionSubscription) this.selectionSubscription.dispose() + if (this.configSubscription) this.configSubscription.dispose() + this.tooltipDisposable.dispose() + } + + subscribeToConfig () { + if (this.configSubscription) this.configSubscription.dispose() + this.configSubscription = atom.config.observe('status-bar.selectionCountFormat', value => { + this.formatString = value || '(%L, %C)' + this.scheduleUpdateCount() + }) + } + + subscribeToActiveTextEditor () { + if (this.selectionSubscription) this.selectionSubscription.dispose() + const selectionsMarkerLayer = this.getActiveTextEditor() && this.getActiveTextEditor().selectionsMarkerLayer + this.selectionSubscription = selectionsMarkerLayer && selectionsMarkerLayer.onDidUpdate(this.scheduleUpdateCount.bind(this)) + this.scheduleUpdateCount() + } + + getActiveTextEditor () { + return atom.workspace.getActiveTextEditor() + } + + scheduleUpdateCount () { + if (!this.scheduledUpdate) { + this.scheduledUpdate = true + return atom.views.updateDocument(() => { + this.updateCount() + this.scheduledUpdate = false + }) + } + } + + updateCount () { + const editor = this.getActiveTextEditor() + const count = editor && editor.getSelectedText().length + const range = editor && editor.getSelectedBufferRange() + let lineCount = range && range.getRowCount() + if (range && range.end.column === 0) lineCount -= 1 + if (count > 0) { + this.element.textContent = this.formatString.replace('%L', lineCount).replace('%C', count) + this.tooltipElement.textContent = `${_.pluralize(lineCount, 'line')}, ${_.pluralize(count, 'character')} selected` + } else { + this.element.textContent = '' + this.tooltipElement.textContent = '' + } + } +} diff --git a/lib/status-bar-view.coffee b/lib/status-bar-view.coffee deleted file mode 100644 index ac707da..0000000 --- a/lib/status-bar-view.coffee +++ /dev/null @@ -1,107 +0,0 @@ -{Disposable} = require 'atom' -Tile = require './tile' - -module.exports = -class StatusBarView - constructor: -> - @element = document.createElement('status-bar') - @element.classList.add('status-bar') - - flexboxHackElement = document.createElement('div') - flexboxHackElement.classList.add('flexbox-repaint-hack') - @element.appendChild(flexboxHackElement) - - @leftPanel = document.createElement('div') - @leftPanel.classList.add('status-bar-left') - flexboxHackElement.appendChild(@leftPanel) - @element.leftPanel = @leftPanel - - @rightPanel = document.createElement('div') - @rightPanel.classList.add('status-bar-right') - flexboxHackElement.appendChild(@rightPanel) - @element.rightPanel = @rightPanel - - @leftTiles = [] - @rightTiles = [] - - @element.getLeftTiles = @getLeftTiles.bind(this) - @element.getRightTiles = @getRightTiles.bind(this) - @element.addLeftTile = @addLeftTile.bind(this) - @element.addRightTile = @addRightTile.bind(this) - - @bufferSubscriptions = [] - - @activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem => - @unsubscribeAllFromBuffer() - @storeActiveBuffer() - @subscribeAllToBuffer() - - @element.dispatchEvent(new CustomEvent('active-buffer-changed', bubbles: true)) - - @storeActiveBuffer() - - destroy: -> - @activeItemSubscription.dispose() - @unsubscribeAllFromBuffer() - @element.remove() - - addLeftTile: (options) -> - newItem = options.item - newPriority = options?.priority ? @leftTiles[@leftTiles.length - 1].priority + 1 - nextItem = null - for {priority, item}, index in @leftTiles - if priority > newPriority - nextItem = item - break - - newTile = new Tile(newItem, newPriority, @leftTiles) - @leftTiles.splice(index, 0, newTile) - newElement = atom.views.getView(newItem) - nextElement = atom.views.getView(nextItem) - @leftPanel.insertBefore(newElement, nextElement) - newTile - - addRightTile: (options) -> - newItem = options.item - newPriority = options?.priority ? @rightTiles[0].priority + 1 - nextItem = null - for {priority, item}, index in @rightTiles - if priority < newPriority - nextItem = item - break - - newTile = new Tile(newItem, newPriority, @rightTiles) - @rightTiles.splice(index, 0, newTile) - newElement = atom.views.getView(newItem) - nextElement = atom.views.getView(nextItem) - @rightPanel.insertBefore(newElement, nextElement) - newTile - - getLeftTiles: -> - @leftTiles - - getRightTiles: -> - @rightTiles - - getActiveBuffer: -> - @buffer - - getActiveItem: -> - atom.workspace.getCenter().getActivePaneItem() - - storeActiveBuffer: -> - @buffer = @getActiveItem()?.getBuffer?() - - subscribeToBuffer: (event, callback) -> - @bufferSubscriptions.push([event, callback]) - @buffer.on(event, callback) if @buffer - - subscribeAllToBuffer: -> - return unless @buffer - for [event, callback] in @bufferSubscriptions - @buffer.on(event, callback) - - unsubscribeAllFromBuffer: -> - return unless @buffer - for [event, callback] in @bufferSubscriptions - @buffer.off(event, callback) diff --git a/lib/status-bar-view.js b/lib/status-bar-view.js new file mode 100644 index 0000000..89bd571 --- /dev/null +++ b/lib/status-bar-view.js @@ -0,0 +1,133 @@ +const Tile = require('./tile') + +module.exports = +class StatusBarView { + constructor () { + this.element = document.createElement('status-bar') + this.element.classList.add('status-bar') + + const flexboxHackElement = document.createElement('div') + flexboxHackElement.classList.add('flexbox-repaint-hack') + this.element.appendChild(flexboxHackElement) + + this.leftPanel = document.createElement('div') + this.leftPanel.classList.add('status-bar-left') + flexboxHackElement.appendChild(this.leftPanel) + this.element.leftPanel = this.leftPanel + + this.rightPanel = document.createElement('div') + this.rightPanel.classList.add('status-bar-right') + flexboxHackElement.appendChild(this.rightPanel) + this.element.rightPanel = this.rightPanel + + this.leftTiles = [] + this.rightTiles = [] + + this.element.getLeftTiles = this.getLeftTiles.bind(this) + this.element.getRightTiles = this.getRightTiles.bind(this) + this.element.addLeftTile = this.addLeftTile.bind(this) + this.element.addRightTile = this.addRightTile.bind(this) + + this.bufferSubscriptions = [] + + this.activeItemSubscription = atom.workspace.getCenter().onDidChangeActivePaneItem(() => { + this.unsubscribeAllFromBuffer() + this.storeActiveBuffer() + this.subscribeAllToBuffer() + + this.element.dispatchEvent(new CustomEvent('active-buffer-changed', {bubbles: true})) + }) + + this.storeActiveBuffer() + } + + destroy () { + this.activeItemSubscription.dispose() + this.unsubscribeAllFromBuffer() + this.element.remove() + } + + addLeftTile (options) { + const newItem = options.item + let newPriority = options.priority + if (newPriority == null) newPriority = this.leftTiles[this.leftTiles.length - 1].priority + 1 + let nextItem = null + let index = 0 + for (index = 0; index < this.leftTiles.length; index++) { + const {priority, item} = this.leftTiles[index] + if (priority > newPriority) { + nextItem = item + break + } + } + + const newTile = new Tile(newItem, newPriority, this.leftTiles) + this.leftTiles.splice(index, 0, newTile) + const newElement = atom.views.getView(newItem) + const nextElement = atom.views.getView(nextItem) + this.leftPanel.insertBefore(newElement, nextElement) + return newTile + } + + addRightTile (options) { + const newItem = options.item + let newPriority = options.priority + if (newPriority == null) newPriority = this.rightTiles[0].priority + 1 + let nextItem = null + let index = 0 + for (index = 0; index < this.rightTiles.length; index++) { + const {priority, item} = this.rightTiles[index] + if (priority < newPriority) { + nextItem = item + break + } + } + + const newTile = new Tile(newItem, newPriority, this.rightTiles) + this.rightTiles.splice(index, 0, newTile) + const newElement = atom.views.getView(newItem) + const nextElement = atom.views.getView(nextItem) + this.rightPanel.insertBefore(newElement, nextElement) + return newTile + } + + getLeftTiles () { + return this.leftTiles + } + + getRightTiles () { + return this.rightTiles + } + + getActiveBuffer () { + return this.buffer + } + + getActiveItem () { + return atom.workspace.getCenter().getActivePaneItem() + } + + storeActiveBuffer () { + const activeItem = this.getActiveItem() + this.buffer = activeItem && typeof activeItem.getBuffer === 'function' && activeItem.getBuffer() + } + + subscribeToBuffer (event, callback) { + this.bufferSubscriptions.push([event, callback]) + if (this.buffer) this.buffer.on(event, callback) + } + + subscribeAllToBuffer () { + if (!this.buffer) return + for (let [event, callback] of this.bufferSubscriptions) { + this.buffer.on(event, callback) + } + } + + unsubscribeAllFromBuffer () { + if (!this.buffer) return + for (let [event, callback] of this.bufferSubscriptions) { + this.buffer.off(event, callback) + } + } +} diff --git a/lib/tile.coffee b/lib/tile.coffee deleted file mode 100644 index b38d3e4..0000000 --- a/lib/tile.coffee +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = -class Tile - constructor: (@item, @priority, @collection) -> - - getItem: -> - @item - - getPriority: -> - @priority - - destroy: -> - @collection.splice(@collection.indexOf(this), 1) - atom.views.getView(@item).remove() diff --git a/lib/tile.js b/lib/tile.js new file mode 100644 index 0000000..de3b73f --- /dev/null +++ b/lib/tile.js @@ -0,0 +1,21 @@ +module.exports = +class Tile { + constructor (item, priority, collection) { + this.item = item + this.priority = priority + this.collection = collection + } + + getItem () { + return this.item + } + + getPriority () { + return this.priority + } + + destroy () { + this.collection.splice(this.collection.indexOf(this), 1) + atom.views.getView(this.item).remove() + } +} diff --git a/package.json b/package.json index 4e4b43f..58c419b 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,20 @@ } }, "devDependencies": { - "coffeelint": "^1.9.7" + "standard": "^10.0.3" + }, + "standard": { + "env": { + "atomtest": true, + "browser": true, + "jasmine": true, + "node": true + }, + "globals": [ + "atom" + ], + "ignore": [ + "spec/fixtures/" + ] } } diff --git a/spec/async-spec-helpers.js b/spec/async-spec-helpers.js new file mode 100644 index 0000000..73002c0 --- /dev/null +++ b/spec/async-spec-helpers.js @@ -0,0 +1,103 @@ +/** @babel */ + +export function beforeEach (fn) { + global.beforeEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +export function afterEach (fn) { + global.afterEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { + module.exports[name] = function (description, fn) { + if (fn === undefined) { + global[name](description) + return + } + + global[name](description, function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) + } +}) + +export async function conditionPromise (condition, description = 'anonymous condition') { + const startTime = Date.now() + + while (true) { + await timeoutPromise(100) + + if (await condition()) { + return + } + + if (Date.now() - startTime > 5000) { + throw new Error('Timed out waiting on ' + description) + } + } +} + +export function timeoutPromise (timeout) { + return new Promise(function (resolve) { + global.setTimeout(resolve, timeout) + }) +} + +function waitsForPromise (fn) { + const promise = fn() + global.waitsFor('spec promise to resolve', function (done) { + promise.then(done, function (error) { + jasmine.getEnv().currentSpec.fail(error) + done() + }) + }) +} + +export function emitterEventPromise (emitter, event, timeout = 15000) { + return new Promise((resolve, reject) => { + const timeoutHandle = setTimeout(() => { + reject(new Error(`Timed out waiting for '${event}' event`)) + }, timeout) + emitter.once(event, () => { + clearTimeout(timeoutHandle) + resolve() + }) + }) +} + +export function promisify (original) { + return function (...args) { + return new Promise((resolve, reject) => { + args.push((err, ...results) => { + if (err) { + reject(err) + } else { + resolve(...results) + } + }) + + return original(...args) + }) + } +} + +export function promisifySome (obj, fnNames) { + const result = {} + for (const fnName of fnNames) { + result[fnName] = promisify(obj[fnName]) + } + return result +} diff --git a/spec/built-in-tiles-spec.coffee b/spec/built-in-tiles-spec.coffee deleted file mode 100644 index 9e9e450..0000000 --- a/spec/built-in-tiles-spec.coffee +++ /dev/null @@ -1,659 +0,0 @@ -fs = require 'fs-plus' -path = require 'path' -os = require 'os' -process = require 'process' - -describe "Built-in Status Bar Tiles", -> - [statusBar, workspaceElement, dummyView] = [] - - beforeEach -> - workspaceElement = atom.views.getView(atom.workspace) - dummyView = document.createElement("div") - statusBar = null - - waitsForPromise -> - atom.packages.activatePackage('status-bar') - - runs -> - statusBar = workspaceElement.querySelector("status-bar") - - describe "the file info, cursor and selection tiles", -> - [editor, buffer, fileInfo, cursorPosition, selectionCount] = [] - - beforeEach -> - waitsForPromise -> - atom.workspace.open('sample.js') - - runs -> - [launchMode, fileInfo, cursorPosition, selectionCount] = - statusBar.getLeftTiles().map (tile) -> tile.getItem() - editor = atom.workspace.getActiveTextEditor() - buffer = editor.getBuffer() - - describe "when associated with an unsaved buffer", -> - it "displays 'untitled' instead of the buffer's path, but still displays the buffer position", -> - waitsForPromise -> - atom.workspace.open() - - runs -> - atom.views.performDocumentUpdate() - expect(fileInfo.currentPath.textContent).toBe 'untitled' - expect(cursorPosition.textContent).toBe '1:1' - expect(selectionCount).toBeHidden() - - describe "when the associated editor's path changes", -> - it "updates the path in the status bar", -> - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - expect(fileInfo.currentPath.textContent).toBe 'sample.txt' - - describe "when associated with remote file path", -> - beforeEach -> - jasmine.attachToDOM(workspaceElement) - dummyView.getPath = -> 'remote://server:123/folder/remote_file.txt' - atom.workspace.getActivePane().activateItem(dummyView) - - it "updates the path in the status bar", -> - # The remote path isn't relativized in the test because no remote directory provider is registered. - expect(fileInfo.currentPath.textContent).toBe 'remote://server:123/folder/remote_file.txt' - expect(fileInfo.currentPath).toBeVisible() - - it "when the path is clicked", -> - fileInfo.currentPath.click() - expect(atom.clipboard.read()).toBe '/folder/remote_file.txt' - - it "calls relativize with the remote URL on shift-click", -> - spy = spyOn(atom.project, 'relativize').andReturn 'remote_file.txt' - event = new MouseEvent('click', shiftKey: true) - fileInfo.currentPath.dispatchEvent(event) - expect(atom.clipboard.read()).toBe 'remote_file.txt' - expect(spy).toHaveBeenCalledWith 'remote://server:123/folder/remote_file.txt' - - describe "when buffer's path is clicked", -> - it "copies the absolute path into the clipboard if available", -> - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - fileInfo.currentPath.click() - expect(atom.clipboard.read()).toBe fileInfo.getActiveItem().getPath() - - describe "when buffer's path is shift-clicked", -> - it "copies the relative path into the clipboard if available", -> - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - event = new MouseEvent('click', shiftKey: true) - fileInfo.currentPath.dispatchEvent(event) - expect(atom.clipboard.read()).toBe 'sample.txt' - - describe "when path of an unsaved buffer is clicked", -> - it "copies the 'untitled' into clipboard", -> - waitsForPromise -> - atom.workspace.open() - - runs -> - fileInfo.currentPath.click() - expect(atom.clipboard.read()).toBe 'untitled' - - describe "when buffer's path is not clicked", -> - it "doesn't display a path tooltip", -> - jasmine.attachToDOM(workspaceElement) - waitsForPromise -> - atom.workspace.open() - - runs -> - expect(document.querySelector('.tooltip')).not.toExist() - - describe "when buffer's path is clicked", -> - it "displays path tooltip and the tooltip disappears after ~2 seconds", -> - jasmine.attachToDOM(workspaceElement) - waitsForPromise -> - atom.workspace.open() - - runs -> - fileInfo.currentPath.click() - expect(document.querySelector('.tooltip')).toBeVisible() - # extra leeway so test won't fail because tooltip disappeared few milliseconds too late - advanceClock(2100) - expect(document.querySelector('.tooltip')).not.toExist() - - describe "when saved buffer's path is clicked", -> - it "displays a tooltip containing text 'Copied:' and an absolute native path", -> - jasmine.attachToDOM(workspaceElement) - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - fileInfo.currentPath.click() - expect(document.querySelector('.tooltip')).toHaveText "Copied: #{fileInfo.getActiveItem().getPath()}" - - it "displays a tooltip containing text 'Copied:' for an absolute Unix path", -> - jasmine.attachToDOM(workspaceElement) - dummyView.getPath = -> '/user/path/for/my/file.txt' - atom.workspace.getActivePane().activateItem(dummyView) - - runs -> - fileInfo.currentPath.click() - expect(document.querySelector('.tooltip')).toHaveText "Copied: #{dummyView.getPath()}" - - it "displays a tooltip containing text 'Copied:' for an absolute Windows path", -> - jasmine.attachToDOM(workspaceElement) - dummyView.getPath = -> 'c:\\user\\path\\for\\my\\file.txt' - atom.workspace.getActivePane().activateItem(dummyView) - - runs -> - fileInfo.currentPath.click() - expect(document.querySelector('.tooltip')).toHaveText "Copied: #{dummyView.getPath()}" - - describe "when unsaved buffer's path is clicked", -> - it "displays a tooltip containing text 'Copied: untitled", -> - jasmine.attachToDOM(workspaceElement) - waitsForPromise -> - atom.workspace.open() - - runs -> - fileInfo.currentPath.click() - expect(document.querySelector('.tooltip')).toHaveText "Copied: untitled" - - describe "when the associated editor's buffer's content changes", -> - it "enables the buffer modified indicator", -> - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - editor.insertText("\n") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(true) - editor.backspace() - - describe "when the buffer content has changed from the content on disk", -> - it "disables the buffer modified indicator on save", -> - filePath = path.join(os.tmpdir(), "atom-whitespace.txt") - fs.writeFileSync(filePath, "") - - waitsForPromise -> - atom.workspace.open(filePath) - - runs -> - editor = atom.workspace.getActiveTextEditor() - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - editor.insertText("\n") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(true) - - waitsForPromise -> - # TODO - remove this Promise.resolve once atom/atom#14435 lands. - Promise.resolve(editor.getBuffer().save()) - - runs -> - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - it "disables the buffer modified indicator if the content matches again", -> - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - editor.insertText("\n") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(true) - editor.backspace() - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - it "disables the buffer modified indicator when the change is undone", -> - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - editor.insertText("\n") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(true) - editor.undo() - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - describe "when the buffer changes", -> - it "updates the buffer modified indicator for the new buffer", -> - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - editor = atom.workspace.getActiveTextEditor() - editor.insertText("\n") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(true) - - it "doesn't update the buffer modified indicator for the old buffer", -> - oldBuffer = editor.getBuffer() - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - waitsForPromise -> - atom.workspace.open('sample.txt') - - runs -> - oldBuffer.setText("new text") - advanceClock(buffer.stoppedChangingDelay) - expect(fileInfo.classList.contains('buffer-modified')).toBe(false) - - describe "when the associated editor's cursor position changes", -> - it "updates the cursor position in the status bar", -> - jasmine.attachToDOM(workspaceElement) - editor.setCursorScreenPosition([1, 2]) - atom.views.performDocumentUpdate() - expect(cursorPosition.textContent).toBe '2:3' - - it "does not throw an exception if the cursor is moved as the result of the active pane item changing to a non-editor (regression)", -> - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage('status-bar')) # Wrapped so works with Promise & non-Promise deactivate - runs -> - atom.workspace.onDidChangeActivePaneItem(-> editor.setCursorScreenPosition([1, 2])) - waitsForPromise -> - atom.packages.activatePackage('status-bar') - runs -> - statusBar = workspaceElement.querySelector("status-bar") - cursorPosition = statusBar.getLeftTiles()[2].getItem() - - atom.workspace.getActivePane().activateItem(document.createElement('div')) - expect(editor.getCursorScreenPosition()).toEqual([1, 2]) - atom.views.performDocumentUpdate() - expect(cursorPosition).toBeHidden() - - describe "when the associated editor's selection changes", -> - it "updates the selection count in the status bar", -> - jasmine.attachToDOM(workspaceElement) - - editor.setSelectedBufferRange([[0, 0], [0, 0]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe '' - - editor.setSelectedBufferRange([[0, 0], [0, 2]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe '(1, 2)' - - editor.setSelectedBufferRange([[0, 0], [1, 30]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "(2, 60)" - - it "does not throw an exception if the cursor is moved as the result of the active pane item changing to a non-editor (regression)", -> - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage('status-bar')) # Wrapped so works with Promise & non-Promise deactivate - runs -> - atom.workspace.onDidChangeActivePaneItem(-> editor.setSelectedBufferRange([[1, 2], [1, 3]])) - waitsForPromise -> - atom.packages.activatePackage('status-bar') - runs -> - statusBar = workspaceElement.querySelector("status-bar") - selectionCount = statusBar.getLeftTiles()[3].getItem() - - atom.workspace.getActivePane().activateItem(document.createElement('div')) - expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 3]]) - atom.views.performDocumentUpdate() - expect(selectionCount).toBeHidden() - - describe "when the active pane item does not implement getCursorBufferPosition()", -> - it "hides the cursor position view", -> - jasmine.attachToDOM(workspaceElement) - atom.workspace.getActivePane().activateItem(dummyView) - atom.views.performDocumentUpdate() - expect(cursorPosition).toBeHidden() - - describe "when the active pane item implements getTitle() but not getPath()", -> - it "displays the title", -> - jasmine.attachToDOM(workspaceElement) - dummyView.getTitle = -> 'View Title' - atom.workspace.getActivePane().activateItem(dummyView) - expect(fileInfo.currentPath.textContent).toBe 'View Title' - expect(fileInfo.currentPath).toBeVisible() - - describe "when the active pane item neither getTitle() nor getPath()", -> - it "hides the path view", -> - jasmine.attachToDOM(workspaceElement) - atom.workspace.getActivePane().activateItem(dummyView) - expect(fileInfo.currentPath).toBeHidden() - - describe "when the active pane item's title changes", -> - it "updates the path view with the new title", -> - jasmine.attachToDOM(workspaceElement) - callbacks = [] - dummyView.onDidChangeTitle = (fn) -> - callbacks.push(fn) - { - dispose: -> - } - dummyView.getTitle = -> 'View Title' - atom.workspace.getActivePane().activateItem(dummyView) - expect(fileInfo.currentPath.textContent).toBe 'View Title' - dummyView.getTitle = -> 'New Title' - callback() for callback in callbacks - expect(fileInfo.currentPath.textContent).toBe 'New Title' - - describe 'the cursor position tile', -> - beforeEach -> - atom.config.set('status-bar.cursorPositionFormat', 'foo %L bar %C') - - it 'respects a format string', -> - jasmine.attachToDOM(workspaceElement) - editor.setCursorScreenPosition([1, 2]) - atom.views.performDocumentUpdate() - expect(cursorPosition.textContent).toBe 'foo 2 bar 3' - - it 'updates when the configuration changes', -> - jasmine.attachToDOM(workspaceElement) - editor.setCursorScreenPosition([1, 2]) - atom.views.performDocumentUpdate() - expect(cursorPosition.textContent).toBe 'foo 2 bar 3' - - atom.config.set('status-bar.cursorPositionFormat', 'baz %C quux %L') - atom.views.performDocumentUpdate() - expect(cursorPosition.textContent).toBe 'baz 3 quux 2' - - describe "when clicked", -> - it "triggers the go-to-line toggle event", -> - eventHandler = jasmine.createSpy('eventHandler') - atom.commands.add('atom-text-editor', 'go-to-line:toggle', eventHandler) - cursorPosition.click() - expect(eventHandler).toHaveBeenCalled() - - describe 'the selection count tile', -> - beforeEach -> - atom.config.set('status-bar.selectionCountFormat', '%L foo %C bar selected') - - it 'respects a format string', -> - jasmine.attachToDOM(workspaceElement) - editor.setSelectedBufferRange([[0, 0], [1, 30]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "2 foo 60 bar selected" - - it 'updates when the configuration changes', -> - jasmine.attachToDOM(workspaceElement) - editor.setSelectedBufferRange([[0, 0], [1, 30]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "2 foo 60 bar selected" - - atom.config.set('status-bar.selectionCountFormat', 'Selection: baz %C quux %L') - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "Selection: baz 60 quux 2" - - it 'does not include the next line if the last selected character is a LF', -> - lineEndingRegExp = /\r\n|\n|\r/g - buffer = editor.getBuffer() - buffer.setText(buffer.getText().replace(lineEndingRegExp, '\n')) - jasmine.attachToDOM(workspaceElement) - editor.setSelectedBufferRange([[0, 0], [1, 0]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "1 foo 30 bar selected" - - it 'does not include the next line if the last selected character is CRLF', -> - lineEndingRegExp = /\r\n|\n|\r/g - buffer = editor.getBuffer() - buffer.setText(buffer.getText().replace(lineEndingRegExp, '\r\n')) - jasmine.attachToDOM(workspaceElement) - editor.setSelectedBufferRange([[0, 0], [1, 0]]) - atom.views.performDocumentUpdate() - expect(selectionCount.textContent).toBe "1 foo 31 bar selected" - - describe "the git tile", -> - gitView = null - - hover = (element, fn) -> - # FIXME: Only use hoverDefaults once Atom 1.13 is on stable - hoverDelay = atom.tooltips.defaults.delay?.show ? atom.tooltips.hoverDefaults.delay.show - element.dispatchEvent(new CustomEvent('mouseenter', bubbles: false)) - element.dispatchEvent(new CustomEvent('mouseover', bubbles: true)) - advanceClock(hoverDelay) - fn() - element.dispatchEvent(new CustomEvent('mouseleave', bubbles: false)) - element.dispatchEvent(new CustomEvent('mouseout', bubbles: true)) - advanceClock(hoverDelay) - - setupWorkingDir = (name) -> - dir = atom.project.getDirectories()[0] - target = "#{os.tmpdir()}/#{name}" - targetGit = target + '/.git' - fs.copySync(dir.resolve('git/working-dir'), path.resolve(target)) - fs.removeSync(path.resolve(targetGit)) - fs.copySync(dir.resolve("git/#{name}.git"), path.resolve(targetGit)) - target - - beforeEach -> - [gitView] = statusBar.getRightTiles().map (tile) -> tile.getItem() - - describe "the git ahead/behind count labels", -> - beforeEach -> - jasmine.attachToDOM(workspaceElement) - - it "shows the number of commits that can be pushed/pulled", -> - workingDir = setupWorkingDir('ahead-behind-repo') - atom.project.setPaths([workingDir]) - filePath = atom.project.getDirectories()[0].resolve('a.txt') - repo = atom.project.getRepositories()[0] - - waitsForPromise -> - atom.workspace.open(filePath) - .then -> repo.refreshStatus() - - runs -> - behindElement = document.body.querySelector(".commits-behind-label") - aheadElement = document.body.querySelector(".commits-ahead-label") - expect(aheadElement).toBeVisible() - expect(behindElement).toBeVisible() - expect(aheadElement.textContent).toContain '1' - - it "stays hidden when no commits can be pushed/pulled", -> - workingDir = setupWorkingDir('no-ahead-behind-repo') - atom.project.setPaths([workingDir]) - filePath = atom.project.getDirectories()[0].resolve('a.txt') - repo = atom.project.getRepositories()[0] - - waitsForPromise -> - atom.workspace.open(filePath) - .then -> repo.refreshStatus() - - runs -> - behindElement = document.body.querySelector(".commits-behind-label") - aheadElement = document.body.querySelector(".commits-ahead-label") - expect(aheadElement).not.toBeVisible() - expect(behindElement).not.toBeVisible() - - describe "the git branch label", -> - projectPath = null - beforeEach -> - projectPath = atom.project.getDirectories()[0].resolve('git/working-dir') - fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) - jasmine.attachToDOM(workspaceElement) - - afterEach -> - fs.moveSync(path.join(projectPath, '.git'), path.join(projectPath, 'git.git')) - - it "displays the current branch for files in repositories", -> - atom.project.setPaths([projectPath]) - - waitsForPromise -> - atom.workspace.open('a.txt') - - runs -> - currentBranch = atom.project.getRepositories()[0].getShortHead() - expect(gitView.branchArea).toBeVisible() - expect(gitView.branchLabel.textContent).toBe currentBranch - - atom.workspace.getActivePane().destroyItems() - expect(gitView.branchArea).toBeVisible() - expect(gitView.branchLabel.textContent).toBe currentBranch - - atom.workspace.getActivePane().activateItem(dummyView) - - runs -> expect(gitView.branchArea).not.toBeVisible() - - it "displays the current branch tooltip", -> - atom.project.setPaths([projectPath]) - - waitsForPromise -> - atom.workspace.open('a.txt') - - runs -> - currentBranch = atom.project.getRepositories()[0].getShortHead() - hover gitView.branchArea, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("On branch #{currentBranch}") - - it "doesn't display the current branch for a file not in a repository", -> - atom.project.setPaths([os.tmpdir()]) - - waitsForPromise -> - atom.workspace.open(path.join(os.tmpdir(), 'temp.txt')) - - runs -> - expect(gitView.branchArea).toBeHidden() - - it "doesn't display the current branch for a file outside the current project", -> - waitsForPromise -> - atom.workspace.open(path.join(os.tmpdir(), 'atom-specs', 'not-in-project.txt')) - - runs -> - expect(gitView.branchArea).toBeHidden() - - describe "the git status label", -> - [repo, filePath, originalPathText, newPath, ignorePath, ignoredPath, projectPath] = [] - - beforeEach -> - projectPath = atom.project.getDirectories()[0].resolve('git/working-dir') - fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) - atom.project.setPaths([projectPath]) - filePath = atom.project.getDirectories()[0].resolve('a.txt') - newPath = atom.project.getDirectories()[0].resolve('new.txt') - fs.writeFileSync(newPath, "I'm new here") - ignorePath = path.join(projectPath, '.gitignore') - fs.writeFileSync(ignorePath, 'ignored.txt') - ignoredPath = path.join(projectPath, 'ignored.txt') - fs.writeFileSync(ignoredPath, '') - jasmine.attachToDOM(workspaceElement) - - repo = atom.project.getRepositories()[0] - originalPathText = fs.readFileSync(filePath, 'utf8') - waitsForPromise -> repo.refreshStatus() - - afterEach -> - fs.writeFileSync(filePath, originalPathText) - fs.removeSync(newPath) - fs.removeSync(ignorePath) - fs.removeSync(ignoredPath) - fs.moveSync(path.join(projectPath, '.git'), path.join(projectPath, 'git.git')) - - it "displays the modified icon for a changed file", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> - fs.writeFileSync(filePath, "i've changed for the worse") - repo.refreshStatus() - runs -> - expect(gitView.gitStatusIcon).toHaveClass('icon-diff-modified') - - it "displays the 1 line added and not committed tooltip", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> - fs.writeFileSync(filePath, "i've changed for the worse") - repo.refreshStatus() - - runs -> - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("1 line added to this file not yet committed") - - it "displays the x lines added and not committed tooltip", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> - fs.writeFileSync(filePath, "i've changed#{os.EOL}for the worse") - repo.refreshStatus() - - runs -> - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("2 lines added to this file not yet committed") - - it "doesn't display the modified icon for an unchanged file", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> repo.refreshStatus() - - runs -> - expect(gitView.gitStatusIcon).toHaveText('') - - it "displays the new icon for a new file", -> - waitsForPromise -> - atom.workspace.open(newPath) - .then -> repo.refreshStatus() - - runs -> - expect(gitView.gitStatusIcon).toHaveClass('icon-diff-added') - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("1 line in this new file not yet committed") - - it "displays the 1 line added and not committed to new file tooltip", -> - waitsForPromise -> - atom.workspace.open(newPath) - .then -> repo.refreshStatus() - - runs -> - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("1 line in this new file not yet committed") - - it "displays the x lines added and not committed to new file tooltip", -> - fs.writeFileSync(newPath, "I'm new#{os.EOL}here") - waitsForPromise -> - atom.workspace.open(newPath) - .then -> repo.refreshStatus() - - runs -> - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("2 lines in this new file not yet committed") - - it "displays the ignored icon for an ignored file", -> - waitsForPromise -> - atom.workspace.open(ignoredPath) - - runs -> - expect(gitView.gitStatusIcon).toHaveClass('icon-diff-ignored') - hover gitView.gitStatusIcon, -> - expect(document.body.querySelector(".tooltip").innerText) - .toBe("File is ignored by git") - - it "updates when a status-changed event occurs", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> - fs.writeFileSync(filePath, "i've changed for the worse") - repo.refreshStatus() - runs -> - expect(gitView.gitStatusIcon).toHaveClass('icon-diff-modified') - - waitsForPromise -> - fs.writeFileSync(filePath, originalPathText) - repo.refreshStatus() - runs -> - expect(gitView.gitStatusIcon).not.toHaveClass('icon-diff-modified') - - it "displays the diff stat for modified files", -> - waitsForPromise -> - atom.workspace.open(filePath) - .then -> - fs.writeFileSync(filePath, "i've changed for the worse") - repo.refreshStatus() - runs -> - expect(gitView.gitStatusIcon).toHaveText('+1') - - it "displays the diff stat for new files", -> - waitsForPromise -> - atom.workspace.open(newPath) - .then -> repo.refreshStatus() - - runs -> - expect(gitView.gitStatusIcon).toHaveText('+1') - - it "does not display for files not in the current project", -> - waitsForPromise -> - atom.workspace.open('/tmp/atom-specs/not-in-project.txt') - - runs -> - expect(gitView.gitStatusIcon).toBeHidden() diff --git a/spec/built-in-tiles-spec.js b/spec/built-in-tiles-spec.js new file mode 100644 index 0000000..2a18328 --- /dev/null +++ b/spec/built-in-tiles-spec.js @@ -0,0 +1,660 @@ +const fs = require('fs-plus') +const path = require('path') +const os = require('os') + +const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars + +describe('Built-in Status Bar Tiles', () => { + let [statusBar, workspaceElement, dummyView] = [] + + beforeEach(async () => { + workspaceElement = atom.views.getView(atom.workspace) + dummyView = document.createElement('div') + + await atom.packages.activatePackage('status-bar') + statusBar = workspaceElement.querySelector('status-bar') + }) + + describe('the file info, cursor and selection tiles', () => { + let [editor, buffer, fileInfo, cursorPosition, selectionCount] = [] + + beforeEach(async () => { + await atom.workspace.open('sample.js') + + let launchMode // eslint-disable-line no-unused-vars + [launchMode, fileInfo, cursorPosition, selectionCount] = statusBar.getLeftTiles().map(tile => tile.getItem()) + editor = atom.workspace.getActiveTextEditor() + buffer = editor.getBuffer() + }) + + describe('when associated with an unsaved buffer', () => { + it("displays 'untitled' instead of the buffer's path, but still displays the buffer position", async () => { + await atom.workspace.open() + + atom.views.performDocumentUpdate() + expect(fileInfo.currentPath.textContent).toBe('untitled') + expect(cursorPosition.textContent).toBe('1:1') + expect(selectionCount).toBeHidden() + }) + }) + + describe("when the associated editor's path changes", () => { + it('updates the path in the status bar', async () => { + await atom.workspace.open('sample.txt') + + expect(fileInfo.currentPath.textContent).toBe('sample.txt') + }) + }) + + describe('when associated with remote file path', () => { + beforeEach(() => { + jasmine.attachToDOM(workspaceElement) + dummyView.getPath = () => 'remote://server:123/folder/remote_file.txt' + atom.workspace.getActivePane().activateItem(dummyView) + }) + + it('updates the path in the status bar', () => { + // The remote path isn't relativized in the test because no remote directory provider is registered. + expect(fileInfo.currentPath.textContent).toBe('remote://server:123/folder/remote_file.txt') + expect(fileInfo.currentPath).toBeVisible() + }) + + it('when the path is clicked', () => { + fileInfo.currentPath.click() + expect(atom.clipboard.read()).toBe('/folder/remote_file.txt') + }) + + it('calls relativize with the remote URL on shift-click', () => { + const spy = spyOn(atom.project, 'relativize').andReturn('remote_file.txt') + const event = new MouseEvent('click', {shiftKey: true}) + fileInfo.currentPath.dispatchEvent(event) + expect(atom.clipboard.read()).toBe('remote_file.txt') + expect(spy).toHaveBeenCalledWith('remote://server:123/folder/remote_file.txt') + }) + }) + + describe("when buffer's path is clicked", () => { + it('copies the absolute path into the clipboard if available', async () => { + await atom.workspace.open('sample.txt') + + fileInfo.currentPath.click() + expect(atom.clipboard.read()).toBe(fileInfo.getActiveItem().getPath()) + }) + }) + + describe("when buffer's path is shift-clicked", () => { + it('copies the relative path into the clipboard if available', async () => { + await atom.workspace.open('sample.txt') + + const event = new MouseEvent('click', {shiftKey: true}) + fileInfo.currentPath.dispatchEvent(event) + expect(atom.clipboard.read()).toBe('sample.txt') + }) + }) + + describe('when path of an unsaved buffer is clicked', () => { + it("copies the 'untitled' into clipboard", async () => { + await atom.workspace.open() + + fileInfo.currentPath.click() + expect(atom.clipboard.read()).toBe('untitled') + }) + }) + + describe("when buffer's path is not clicked", () => { + it("doesn't display a path tooltip", async () => { + jasmine.attachToDOM(workspaceElement) + await atom.workspace.open() + + expect(document.querySelector('.tooltip')).not.toExist() + }) + }) + + describe("when buffer's path is clicked", () => { + it('displays path tooltip and the tooltip disappears after ~2 seconds', async () => { + jasmine.attachToDOM(workspaceElement) + await atom.workspace.open() + + fileInfo.currentPath.click() + expect(document.querySelector('.tooltip')).toBeVisible() + // extra leeway so test won't fail because tooltip disappeared few milliseconds too late + advanceClock(2100) + expect(document.querySelector('.tooltip')).not.toExist() + }) + }) + + describe("when saved buffer's path is clicked", () => { + it("displays a tooltip containing text 'Copied:' and an absolute native path", async () => { + jasmine.attachToDOM(workspaceElement) + await atom.workspace.open('sample.txt') + + fileInfo.currentPath.click() + expect(document.querySelector('.tooltip')).toHaveText(`Copied: ${fileInfo.getActiveItem().getPath()}`) + }) + + it("displays a tooltip containing text 'Copied:' for an absolute Unix path", () => { + jasmine.attachToDOM(workspaceElement) + dummyView.getPath = () => '/user/path/for/my/file.txt' + atom.workspace.getActivePane().activateItem(dummyView) + + fileInfo.currentPath.click() + expect(document.querySelector('.tooltip')).toHaveText(`Copied: ${dummyView.getPath()}`) + }) + + it("displays a tooltip containing text 'Copied:' for an absolute Windows path", () => { + jasmine.attachToDOM(workspaceElement) + dummyView.getPath = () => 'c:\\user\\path\\for\\my\\file.txt' + atom.workspace.getActivePane().activateItem(dummyView) + + fileInfo.currentPath.click() + expect(document.querySelector('.tooltip')).toHaveText(`Copied: ${dummyView.getPath()}`) + }) + }) + + describe("when unsaved buffer's path is clicked", () => { + it("displays a tooltip containing text 'Copied: untitled", async () => { + jasmine.attachToDOM(workspaceElement) + await atom.workspace.open() + + fileInfo.currentPath.click() + expect(document.querySelector('.tooltip')).toHaveText('Copied: untitled') + }) + }) + + describe("when the associated editor's buffer's content changes", () => { + it('enables the buffer modified indicator', () => { + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + editor.insertText('\n') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(true) + editor.backspace() + }) + }) + + describe('when the buffer content has changed from the content on disk', () => { + it('disables the buffer modified indicator on save', async () => { + const filePath = path.join(os.tmpdir(), 'atom-whitespace.txt') + fs.writeFileSync(filePath, '') + + await atom.workspace.open(filePath) + + editor = atom.workspace.getActiveTextEditor() + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + editor.insertText('\n') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(true) + + await editor.getBuffer().save() + + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + }) + + it('disables the buffer modified indicator if the content matches again', () => { + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + editor.insertText('\n') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(true) + editor.backspace() + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + }) + + it('disables the buffer modified indicator when the change is undone', () => { + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + editor.insertText('\n') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(true) + editor.undo() + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + }) + }) + + describe('when the buffer changes', () => { + it('updates the buffer modified indicator for the new buffer', async () => { + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + + await atom.workspace.open('sample.txt') + + editor = atom.workspace.getActiveTextEditor() + editor.insertText('\n') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(true) + }) + + it("doesn't update the buffer modified indicator for the old buffer", async () => { + const oldBuffer = editor.getBuffer() + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + + await atom.workspace.open('sample.txt') + + oldBuffer.setText('new text') + advanceClock(buffer.stoppedChangingDelay) + expect(fileInfo.classList.contains('buffer-modified')).toBe(false) + }) + }) + + describe("when the associated editor's cursor position changes", () => { + it('updates the cursor position in the status bar', () => { + jasmine.attachToDOM(workspaceElement) + editor.setCursorScreenPosition([1, 2]) + atom.views.performDocumentUpdate() + expect(cursorPosition.textContent).toBe('2:3') + }) + + it('does not throw an exception if the cursor is moved as the result of the active pane item changing to a non-editor (regression)', async () => { + await atom.packages.deactivatePackage('status-bar') + atom.workspace.onDidChangeActivePaneItem(() => editor.setCursorScreenPosition([1, 2])) + await atom.packages.activatePackage('status-bar') + statusBar = workspaceElement.querySelector('status-bar') + cursorPosition = statusBar.getLeftTiles()[2].getItem() + + atom.workspace.getActivePane().activateItem(document.createElement('div')) + expect(editor.getCursorScreenPosition()).toEqual([1, 2]) + atom.views.performDocumentUpdate() + expect(cursorPosition).toBeHidden() + }) + }) + + describe("when the associated editor's selection changes", () => { + it('updates the selection count in the status bar', () => { + jasmine.attachToDOM(workspaceElement) + + editor.setSelectedBufferRange([[0, 0], [0, 0]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('') + + editor.setSelectedBufferRange([[0, 0], [0, 2]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('(1, 2)') + + editor.setSelectedBufferRange([[0, 0], [1, 30]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('(2, 60)') + }) + + it('does not throw an exception if the cursor is moved as the result of the active pane item changing to a non-editor (regression)', async () => { + await atom.packages.deactivatePackage('status-bar') + atom.workspace.onDidChangeActivePaneItem(() => editor.setSelectedBufferRange([[1, 2], [1, 3]])) + await atom.packages.activatePackage('status-bar') + statusBar = workspaceElement.querySelector('status-bar') + selectionCount = statusBar.getLeftTiles()[3].getItem() + + atom.workspace.getActivePane().activateItem(document.createElement('div')) + expect(editor.getSelectedBufferRange()).toEqual([[1, 2], [1, 3]]) + atom.views.performDocumentUpdate() + expect(selectionCount).toBeHidden() + }) + }) + + describe('when the active pane item does not implement getCursorBufferPosition()', () => { + it('hides the cursor position view', () => { + jasmine.attachToDOM(workspaceElement) + atom.workspace.getActivePane().activateItem(dummyView) + atom.views.performDocumentUpdate() + expect(cursorPosition).toBeHidden() + }) + }) + + describe('when the active pane item implements getTitle() but not getPath()', () => { + it('displays the title', () => { + jasmine.attachToDOM(workspaceElement) + dummyView.getTitle = () => 'View Title' + atom.workspace.getActivePane().activateItem(dummyView) + expect(fileInfo.currentPath.textContent).toBe('View Title') + expect(fileInfo.currentPath).toBeVisible() + }) + }) + + describe('when the active pane item neither getTitle() nor getPath()', () => { + it('hides the path view', () => { + jasmine.attachToDOM(workspaceElement) + atom.workspace.getActivePane().activateItem(dummyView) + expect(fileInfo.currentPath).toBeHidden() + }) + }) + + describe("when the active pane item's title changes", () => { + it('updates the path view with the new title', () => { + jasmine.attachToDOM(workspaceElement) + const callbacks = [] + dummyView.onDidChangeTitle = fn => { + callbacks.push(fn) + return { + dispose () {} + } + } + dummyView.getTitle = () => 'View Title' + atom.workspace.getActivePane().activateItem(dummyView) + expect(fileInfo.currentPath.textContent).toBe('View Title') + dummyView.getTitle = () => 'New Title' + for (let callback of callbacks) { callback() } + expect(fileInfo.currentPath.textContent).toBe('New Title') + }) + }) + + describe('the cursor position tile', () => { + beforeEach(() => atom.config.set('status-bar.cursorPositionFormat', 'foo %L bar %C')) + + it('respects a format string', () => { + jasmine.attachToDOM(workspaceElement) + editor.setCursorScreenPosition([1, 2]) + atom.views.performDocumentUpdate() + expect(cursorPosition.textContent).toBe('foo 2 bar 3') + }) + + it('updates when the configuration changes', () => { + jasmine.attachToDOM(workspaceElement) + editor.setCursorScreenPosition([1, 2]) + atom.views.performDocumentUpdate() + expect(cursorPosition.textContent).toBe('foo 2 bar 3') + + atom.config.set('status-bar.cursorPositionFormat', 'baz %C quux %L') + atom.views.performDocumentUpdate() + expect(cursorPosition.textContent).toBe('baz 3 quux 2') + }) + + describe('when clicked', () => + it('triggers the go-to-line toggle event', () => { + const eventHandler = jasmine.createSpy('eventHandler') + atom.commands.add('atom-text-editor', 'go-to-line:toggle', eventHandler) + cursorPosition.click() + expect(eventHandler).toHaveBeenCalled() + }) + ) + }) + + describe('the selection count tile', () => { + beforeEach(() => atom.config.set('status-bar.selectionCountFormat', '%L foo %C bar selected')) + + it('respects a format string', () => { + jasmine.attachToDOM(workspaceElement) + editor.setSelectedBufferRange([[0, 0], [1, 30]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('2 foo 60 bar selected') + }) + + it('updates when the configuration changes', () => { + jasmine.attachToDOM(workspaceElement) + editor.setSelectedBufferRange([[0, 0], [1, 30]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('2 foo 60 bar selected') + + atom.config.set('status-bar.selectionCountFormat', 'Selection: baz %C quux %L') + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('Selection: baz 60 quux 2') + }) + + it('does not include the next line if the last selected character is a LF', () => { + const lineEndingRegExp = /\r\n|\n|\r/g + buffer = editor.getBuffer() + buffer.setText(buffer.getText().replace(lineEndingRegExp, '\n')) + jasmine.attachToDOM(workspaceElement) + editor.setSelectedBufferRange([[0, 0], [1, 0]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('1 foo 30 bar selected') + }) + + it('does not include the next line if the last selected character is CRLF', () => { + const lineEndingRegExp = /\r\n|\n|\r/g + buffer = editor.getBuffer() + buffer.setText(buffer.getText().replace(lineEndingRegExp, '\r\n')) + jasmine.attachToDOM(workspaceElement) + editor.setSelectedBufferRange([[0, 0], [1, 0]]) + atom.views.performDocumentUpdate() + expect(selectionCount.textContent).toBe('1 foo 31 bar selected') + }) + }) + }) + + describe('the git tile', () => { + let gitView = null + + const hover = (element, fn) => { + const hoverDelay = atom.tooltips.hoverDefaults.delay.show + element.dispatchEvent(new CustomEvent('mouseenter', {bubbles: false})) + element.dispatchEvent(new CustomEvent('mouseover', {bubbles: true})) + advanceClock(hoverDelay) + fn() + element.dispatchEvent(new CustomEvent('mouseleave', {bubbles: false})) + element.dispatchEvent(new CustomEvent('mouseout', {bubbles: true})) + advanceClock(hoverDelay) + } + + const setupWorkingDir = (name) => { + const dir = atom.project.getDirectories()[0] + const target = `${os.tmpdir()}/${name}` + const targetGit = target + '/.git' + fs.copySync(dir.resolve('git/working-dir'), path.resolve(target)) + fs.removeSync(path.resolve(targetGit)) + fs.copySync(dir.resolve(`git/${name}.git`), path.resolve(targetGit)) + return target + } + + beforeEach(() => { + [gitView] = statusBar.getRightTiles().map(tile => tile.getItem()) + }) + + describe('the git ahead/behind count labels', () => { + beforeEach(() => jasmine.attachToDOM(workspaceElement)) + + it('shows the number of commits that can be pushed/pulled', async () => { + const workingDir = setupWorkingDir('ahead-behind-repo') + atom.project.setPaths([workingDir]) + const filePath = atom.project.getDirectories()[0].resolve('a.txt') + const repo = atom.project.getRepositories()[0] + + await atom.workspace.open(filePath) + await repo.refreshStatus() + + const behindElement = document.body.querySelector('.commits-behind-label') + const aheadElement = document.body.querySelector('.commits-ahead-label') + expect(aheadElement).toBeVisible() + expect(behindElement).toBeVisible() + expect(aheadElement.textContent).toContain('1') + }) + + it('stays hidden when no commits can be pushed/pulled', async () => { + const workingDir = setupWorkingDir('no-ahead-behind-repo') + atom.project.setPaths([workingDir]) + const filePath = atom.project.getDirectories()[0].resolve('a.txt') + const repo = atom.project.getRepositories()[0] + + await atom.workspace.open(filePath) + await repo.refreshStatus() + + const behindElement = document.body.querySelector('.commits-behind-label') + const aheadElement = document.body.querySelector('.commits-ahead-label') + expect(aheadElement).not.toBeVisible() + expect(behindElement).not.toBeVisible() + }) + }) + + describe('the git branch label', () => { + let projectPath = null + beforeEach(() => { + projectPath = atom.project.getDirectories()[0].resolve('git/working-dir') + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + jasmine.attachToDOM(workspaceElement) + }) + + afterEach(() => fs.moveSync(path.join(projectPath, '.git'), path.join(projectPath, 'git.git'))) + + it('displays the current branch for files in repositories', async () => { + atom.project.setPaths([projectPath]) + + await atom.workspace.open('a.txt') + + const currentBranch = atom.project.getRepositories()[0].getShortHead() + expect(gitView.branchArea).toBeVisible() + expect(gitView.branchLabel.textContent).toBe(currentBranch) + + atom.workspace.getActivePane().destroyItems() + expect(gitView.branchArea).toBeVisible() + expect(gitView.branchLabel.textContent).toBe(currentBranch) + + atom.workspace.getActivePane().activateItem(dummyView) + + expect(gitView.branchArea).not.toBeVisible() + }) + + it('displays the current branch tooltip', async () => { + atom.project.setPaths([projectPath]) + + await atom.workspace.open('a.txt') + + const currentBranch = atom.project.getRepositories()[0].getShortHead() + hover(gitView.branchArea, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe(`On branch ${currentBranch}`) + }) + }) + + it("doesn't display the current branch for a file not in a repository", async () => { + atom.project.setPaths([os.tmpdir()]) + + await atom.workspace.open(path.join(os.tmpdir(), 'temp.txt')) + + expect(gitView.branchArea).toBeHidden() + }) + + it("doesn't display the current branch for a file outside the current project", async () => { + await atom.workspace.open(path.join(os.tmpdir(), 'atom-specs', 'not-in-project.txt')) + + expect(gitView.branchArea).toBeHidden() + }) + }) + + describe('the git status label', () => { + let [repo, filePath, originalPathText, newPath, ignorePath, ignoredPath, projectPath] = [] + + beforeEach(async () => { + projectPath = atom.project.getDirectories()[0].resolve('git/working-dir') + fs.moveSync(path.join(projectPath, 'git.git'), path.join(projectPath, '.git')) + atom.project.setPaths([projectPath]) + filePath = atom.project.getDirectories()[0].resolve('a.txt') + newPath = atom.project.getDirectories()[0].resolve('new.txt') + fs.writeFileSync(newPath, "I'm new here") + ignorePath = path.join(projectPath, '.gitignore') + fs.writeFileSync(ignorePath, 'ignored.txt') + ignoredPath = path.join(projectPath, 'ignored.txt') + fs.writeFileSync(ignoredPath, '') + jasmine.attachToDOM(workspaceElement) + + repo = atom.project.getRepositories()[0] + originalPathText = fs.readFileSync(filePath, 'utf8') + await repo.refreshStatus() + }) + + afterEach(() => { + fs.writeFileSync(filePath, originalPathText) + fs.removeSync(newPath) + fs.removeSync(ignorePath) + fs.removeSync(ignoredPath) + fs.moveSync(path.join(projectPath, '.git'), path.join(projectPath, 'git.git')) + }) + + it('displays the modified icon for a changed file', async () => { + await atom.workspace.open(filePath) + fs.writeFileSync(filePath, "i've changed for the worse") + await repo.refreshStatus() + expect(gitView.gitStatusIcon).toHaveClass('icon-diff-modified') + }) + + it('displays the 1 line added and not committed tooltip', async () => { + await atom.workspace.open(filePath) + fs.writeFileSync(filePath, "i've changed for the worse") + await repo.refreshStatus() + + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('1 line added to this file not yet committed') + }) + }) + + it('displays the x lines added and not committed tooltip', async () => { + await atom.workspace.open(filePath) + fs.writeFileSync(filePath, `i've changed${os.EOL}for the worse`) + await repo.refreshStatus() + + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('2 lines added to this file not yet committed') + }) + }) + + it("doesn't display the modified icon for an unchanged file", async () => { + await atom.workspace.open(filePath) + await repo.refreshStatus() + + expect(gitView.gitStatusIcon).toHaveText('') + }) + + it('displays the new icon for a new file', async () => { + await atom.workspace.open(newPath) + await repo.refreshStatus() + + expect(gitView.gitStatusIcon).toHaveClass('icon-diff-added') + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('1 line in this new file not yet committed') + }) + }) + + it('displays the 1 line added and not committed to new file tooltip', async () => { + await atom.workspace.open(newPath) + await repo.refreshStatus() + + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('1 line in this new file not yet committed') + }) + }) + + it('displays the x lines added and not committed to new file tooltip', async () => { + fs.writeFileSync(newPath, `I'm new${os.EOL}here`) + await atom.workspace.open(newPath) + await repo.refreshStatus() + + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('2 lines in this new file not yet committed') + }) + }) + + it('displays the ignored icon for an ignored file', async () => { + await atom.workspace.open(ignoredPath) + + expect(gitView.gitStatusIcon).toHaveClass('icon-diff-ignored') + hover(gitView.gitStatusIcon, () => { + expect(document.body.querySelector('.tooltip').innerText).toBe('File is ignored by git') + }) + }) + + it('updates when a status-changed event occurs', async () => { + await atom.workspace.open(filePath) + fs.writeFileSync(filePath, "i've changed for the worse") + await repo.refreshStatus() + expect(gitView.gitStatusIcon).toHaveClass('icon-diff-modified') + + fs.writeFileSync(filePath, originalPathText) + await repo.refreshStatus() + expect(gitView.gitStatusIcon).not.toHaveClass('icon-diff-modified') + }) + + it('displays the diff stat for modified files', async () => { + await atom.workspace.open(filePath) + fs.writeFileSync(filePath, "i've changed for the worse") + await repo.refreshStatus() + expect(gitView.gitStatusIcon).toHaveText('+1') + }) + + it('displays the diff stat for new files', async () => { + await atom.workspace.open(newPath) + await repo.refreshStatus() + + expect(gitView.gitStatusIcon).toHaveText('+1') + }) + + it('does not display for files not in the current project', async () => { + await atom.workspace.open('/tmp/atom-specs/not-in-project.txt') + + expect(gitView.gitStatusIcon).toBeHidden() + }) + }) + }) +}) diff --git a/spec/fixtures/sample.js b/spec/fixtures/sample.js index fb33b0b..98d50c9 100644 --- a/spec/fixtures/sample.js +++ b/spec/fixtures/sample.js @@ -1,13 +1,13 @@ var quicksort = function () { - var sort = function(items) { - if (items.length <= 1) return items; - var pivot = items.shift(), current, left = [], right = []; - while(items.length > 0) { - current = items.shift(); - current < pivot ? left.push(current) : right.push(current); + var sort = function (items) { + if (items.length <= 1) return items + var pivot = items.shift(), current, left = [], right = [] + while (items.length > 0) { + current = items.shift() + current < pivot ? left.push(current) : right.push(current) } - return sort(left).concat(pivot).concat(sort(right)); - }; + return sort(left).concat(pivot).concat(sort(right)) + } - return sort(Array.apply(this, arguments)); -}; \ No newline at end of file + return sort(Array.apply(this, arguments)) +} diff --git a/spec/status-bar-spec.coffee b/spec/status-bar-spec.coffee deleted file mode 100644 index dee7358..0000000 --- a/spec/status-bar-spec.coffee +++ /dev/null @@ -1,111 +0,0 @@ -describe "Status Bar package", -> - [editor, statusBar, statusBarService, workspaceElement, mainModule] = [] - - beforeEach -> - workspaceElement = atom.views.getView(atom.workspace) - - waitsForPromise -> - atom.packages.activatePackage('status-bar').then (pack) -> - statusBar = workspaceElement.querySelector("status-bar") - statusBarService = pack.mainModule.provideStatusBar() - {mainModule} = pack - - describe "@activate()", -> - it "appends only one status bar", -> - expect(workspaceElement.querySelectorAll('status-bar').length).toBe 1 - atom.workspace.getActivePane().splitRight(copyActiveItem: true) - expect(workspaceElement.querySelectorAll('status-bar').length).toBe 1 - - describe "@deactivate()", -> - it "removes the status bar view", -> - waitsForPromise -> - Promise.resolve(atom.packages.deactivatePackage('status-bar')) # Wrapped so works with Promise & non-Promise deactivate - runs -> - expect(workspaceElement.querySelector('status-bar')).toBeNull() - - describe "isVisible option", -> - beforeEach -> - jasmine.attachToDOM(workspaceElement) - - describe "when it is true", -> - beforeEach -> - atom.config.set 'status-bar.isVisible', true - - it "shows status bar", -> - expect(workspaceElement.querySelector('status-bar').parentNode).toBeVisible() - - describe "when it is false", -> - beforeEach -> - atom.config.set 'status-bar.isVisible', false - - it "hides status bar", -> - expect(workspaceElement.querySelector('status-bar').parentNode).not.toBeVisible() - - describe "when status-bar:toggle is triggered", -> - beforeEach -> - jasmine.attachToDOM(workspaceElement) - atom.config.set 'status-bar.isVisible', true - - it "hides or shows the status bar", -> - atom.commands.dispatch(workspaceElement, 'status-bar:toggle') - expect(workspaceElement.querySelector('status-bar').parentNode).not.toBeVisible() - atom.commands.dispatch(workspaceElement, 'status-bar:toggle') - expect(workspaceElement.querySelector('status-bar').parentNode).toBeVisible() - - it "toggles the value of isVisible in config file", -> - expect(atom.config.get 'status-bar.isVisible').toBe true - atom.commands.dispatch(workspaceElement, 'status-bar:toggle') - expect(atom.config.get 'status-bar.isVisible').toBe false - atom.commands.dispatch(workspaceElement, 'status-bar:toggle') - expect(atom.config.get 'status-bar.isVisible').toBe true - - describe "full-width setting", -> - [containers] = [] - - beforeEach -> - containers = atom.workspace.panelContainers - jasmine.attachToDOM(workspaceElement) - - waitsForPromise -> - atom.workspace.open('sample.js') - - it "expects the setting to be enabled by default", -> - expect(atom.config.get('status-bar.fullWidth')).toBeTruthy() - expect(containers.footer.panels).toContain(mainModule.statusBarPanel) - - describe "when setting is changed", -> - it "fits status bar to editor's width", -> - atom.config.set('status-bar.fullWidth', false) - expect(containers.bottom.panels).toContain(mainModule.statusBarPanel) - expect(containers.footer.panels).not.toContain(mainModule.statusBarPanel) - - it "restores the status-bar location when re-enabling setting", -> - atom.config.set('status-bar.fullWidth', true) - expect(containers.footer.panels).toContain(mainModule.statusBarPanel) - expect(containers.bottom.panels).not.toContain(mainModule.statusBarPanel) - - describe "the 'status-bar' service", -> - it "allows tiles to be added, removed, and retrieved", -> - dummyView = document.createElement("div") - tile = statusBarService.addLeftTile(item: dummyView) - expect(statusBar).toContain(dummyView) - expect(statusBarService.getLeftTiles()).toContain(tile) - tile.destroy() - expect(statusBar).not.toContain(dummyView) - expect(statusBarService.getLeftTiles()).not.toContain(tile) - - dummyView = document.createElement("div") - tile = statusBarService.addRightTile(item: dummyView) - expect(statusBar).toContain(dummyView) - expect(statusBarService.getRightTiles()).toContain(tile) - tile.destroy() - expect(statusBar).not.toContain(dummyView) - expect(statusBarService.getRightTiles()).not.toContain(tile) - - it "allows the git info tile to be disabled", -> - getGitInfoTile = -> - statusBar.getRightTiles().find((tile) -> tile.item.matches('.git-view')) - - expect(getGitInfoTile()).not.toBeUndefined() - statusBarService.disableGitInfoTile() - expect(getGitInfoTile()).toBeUndefined() diff --git a/spec/status-bar-spec.js b/spec/status-bar-spec.js new file mode 100644 index 0000000..279b32d --- /dev/null +++ b/spec/status-bar-spec.js @@ -0,0 +1,125 @@ +const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars + +describe('Status Bar package', () => { + let [statusBar, statusBarService, workspaceElement, mainModule] = [] + + beforeEach(async () => { + workspaceElement = atom.views.getView(atom.workspace) + + const pack = await atom.packages.activatePackage('status-bar') + mainModule = pack.mainModule + statusBar = workspaceElement.querySelector('status-bar') + statusBarService = mainModule.provideStatusBar() + }) + + describe('@activate()', () => { + it('appends only one status bar', () => { + expect(workspaceElement.querySelectorAll('status-bar').length).toBe(1) + atom.workspace.getActivePane().splitRight({copyActiveItem: true}) + expect(workspaceElement.querySelectorAll('status-bar').length).toBe(1) + }) + }) + + describe('@deactivate()', () => { + it('removes the status bar view', async () => { + await atom.packages.deactivatePackage('status-bar') + expect(workspaceElement.querySelector('status-bar')).toBeNull() + }) + }) + + describe('isVisible option', () => { + beforeEach(() => jasmine.attachToDOM(workspaceElement)) + + describe('when it is true', () => { + beforeEach(() => atom.config.set('status-bar.isVisible', true)) + + it('shows status bar', () => expect(workspaceElement.querySelector('status-bar').parentNode).toBeVisible()) + }) + + describe('when it is false', () => { + beforeEach(() => atom.config.set('status-bar.isVisible', false)) + + it('hides status bar', () => expect(workspaceElement.querySelector('status-bar').parentNode).not.toBeVisible()) + }) + }) + + describe('when status-bar:toggle is triggered', () => { + beforeEach(() => { + jasmine.attachToDOM(workspaceElement) + atom.config.set('status-bar.isVisible', true) + }) + + it('hides or shows the status bar', () => { + atom.commands.dispatch(workspaceElement, 'status-bar:toggle') + expect(workspaceElement.querySelector('status-bar').parentNode).not.toBeVisible() + atom.commands.dispatch(workspaceElement, 'status-bar:toggle') + expect(workspaceElement.querySelector('status-bar').parentNode).toBeVisible() + }) + + it('toggles the value of isVisible in config file', () => { + expect(atom.config.get('status-bar.isVisible')).toBe(true) + atom.commands.dispatch(workspaceElement, 'status-bar:toggle') + expect(atom.config.get('status-bar.isVisible')).toBe(false) + atom.commands.dispatch(workspaceElement, 'status-bar:toggle') + expect(atom.config.get('status-bar.isVisible')).toBe(true) + }) + }) + + describe('full-width setting', () => { + let [containers] = [] + + beforeEach(async () => { + containers = atom.workspace.panelContainers + jasmine.attachToDOM(workspaceElement) + + await atom.workspace.open('sample.js') + }) + + it('expects the setting to be enabled by default', () => { + expect(atom.config.get('status-bar.fullWidth')).toBeTruthy() + expect(containers.footer.panels).toContain(mainModule.statusBarPanel) + }) + + describe('when setting is changed', () => { + it("fits status bar to editor's width", () => { + atom.config.set('status-bar.fullWidth', false) + expect(containers.bottom.panels).toContain(mainModule.statusBarPanel) + expect(containers.footer.panels).not.toContain(mainModule.statusBarPanel) + }) + + it('restores the status-bar location when re-enabling setting', () => { + atom.config.set('status-bar.fullWidth', true) + expect(containers.footer.panels).toContain(mainModule.statusBarPanel) + expect(containers.bottom.panels).not.toContain(mainModule.statusBarPanel) + }) + }) + }) + + describe("the 'status-bar' service", () => { + it('allows tiles to be added, removed, and retrieved', () => { + let dummyView = document.createElement('div') + let tile = statusBarService.addLeftTile({item: dummyView}) + expect(statusBar).toContain(dummyView) + expect(statusBarService.getLeftTiles()).toContain(tile) + tile.destroy() + expect(statusBar).not.toContain(dummyView) + expect(statusBarService.getLeftTiles()).not.toContain(tile) + + dummyView = document.createElement('div') + tile = statusBarService.addRightTile({item: dummyView}) + expect(statusBar).toContain(dummyView) + expect(statusBarService.getRightTiles()).toContain(tile) + tile.destroy() + expect(statusBar).not.toContain(dummyView) + expect(statusBarService.getRightTiles()).not.toContain(tile) + }) + + it('allows the git info tile to be disabled', () => { + const getGitInfoTile = () => statusBar.getRightTiles().find(tile => tile.item.matches('.git-view')) + + expect(getGitInfoTile()).not.toBeUndefined() + statusBarService.disableGitInfoTile() + expect(getGitInfoTile()).toBeUndefined() + }) + }) +}) diff --git a/spec/status-bar-view-spec.coffee b/spec/status-bar-view-spec.coffee deleted file mode 100644 index ae2acd3..0000000 --- a/spec/status-bar-view-spec.coffee +++ /dev/null @@ -1,103 +0,0 @@ -StatusBarView = require '../lib/status-bar-view' - -describe "StatusBarView", -> - statusBarView = null - - class TestItem - constructor: (@id) -> - - beforeEach -> - statusBarView = new StatusBarView() - - atom.views.addViewProvider TestItem, (model) -> - element = document.createElement("item-view") - element.model = model - element - - describe "::addLeftTile({item, priority})", -> - it "appends the view for the given item to its left side", -> - testItem1 = new TestItem(1) - testItem2 = new TestItem(2) - testItem3 = new TestItem(3) - - tile1 = statusBarView.addLeftTile(item: testItem1, priority: 10) - tile2 = statusBarView.addLeftTile(item: testItem2, priority: 30) - tile3 = statusBarView.addLeftTile(item: testItem3, priority: 20) - - {leftPanel} = statusBarView - - expect(leftPanel.children[0].nodeName).toBe("ITEM-VIEW") - expect(leftPanel.children[1].nodeName).toBe("ITEM-VIEW") - expect(leftPanel.children[2].nodeName).toBe("ITEM-VIEW") - - expect(leftPanel.children[0].model).toBe(testItem1) - expect(leftPanel.children[1].model).toBe(testItem3) - expect(leftPanel.children[2].model).toBe(testItem2) - - expect(statusBarView.getLeftTiles()).toEqual([tile1, tile3, tile2]) - expect(tile1.getPriority()).toBe(10) - expect(tile1.getItem()).toBe(testItem1) - - it "allows the view to be removed", -> - testItem = new TestItem(1) - tile = statusBarView.addLeftTile(item: testItem, priority: 10) - tile.destroy() - expect(statusBarView.leftPanel.children.length).toBe(0) - - statusBarView.addLeftTile(item: testItem, priority: 9) - - describe "when no priority is given", -> - it "appends the item", -> - testItem1 = new TestItem(1) - testItem2 = new TestItem(2) - - statusBarView.addLeftTile(item: testItem1, priority: 1000) - statusBarView.addLeftTile(item: testItem2) - - {leftPanel} = statusBarView - expect(leftPanel.children[0].model).toBe(testItem1) - expect(leftPanel.children[1].model).toBe(testItem2) - - describe "::addRightTile({item, priority})", -> - it "appends the view for the given item to its right side", -> - testItem1 = new TestItem(1) - testItem2 = new TestItem(2) - testItem3 = new TestItem(3) - - tile1 = statusBarView.addRightTile(item: testItem1, priority: 10) - tile2 = statusBarView.addRightTile(item: testItem2, priority: 30) - tile3 = statusBarView.addRightTile(item: testItem3, priority: 20) - - {rightPanel} = statusBarView - - expect(rightPanel.children[0].nodeName).toBe("ITEM-VIEW") - expect(rightPanel.children[1].nodeName).toBe("ITEM-VIEW") - expect(rightPanel.children[2].nodeName).toBe("ITEM-VIEW") - - expect(rightPanel.children[0].model).toBe(testItem2) - expect(rightPanel.children[1].model).toBe(testItem3) - expect(rightPanel.children[2].model).toBe(testItem1) - - expect(statusBarView.getRightTiles()).toEqual([tile2, tile3, tile1]) - expect(tile1.getPriority()).toBe(10) - expect(tile1.getItem()).toBe(testItem1) - - it "allows the view to be removed", -> - testItem = new TestItem(1) - disposable = statusBarView.addRightTile(item: testItem, priority: 10) - disposable.destroy() - expect(statusBarView.rightPanel.children.length).toBe(0) - - statusBarView.addRightTile(item: testItem, priority: 11) - - describe "when no priority is given", -> - it "prepends the item", -> - testItem1 = new TestItem(1, priority: 1000) - testItem2 = new TestItem(2) - - statusBarView.addRightTile(item: testItem1, priority: 1000) - statusBarView.addRightTile(item: testItem2) - - {rightPanel} = statusBarView - expect(rightPanel.children[0].model).toBe(testItem2) - expect(rightPanel.children[1].model).toBe(testItem1) diff --git a/spec/status-bar-view-spec.js b/spec/status-bar-view-spec.js new file mode 100644 index 0000000..44757a7 --- /dev/null +++ b/spec/status-bar-view-spec.js @@ -0,0 +1,120 @@ +const StatusBarView = require('../lib/status-bar-view') +const {it, fit, ffit, afterEach, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars + +describe('StatusBarView', () => { + let statusBarView = null + + class TestItem { + constructor (id) { + this.id = id + } + } + + beforeEach(() => { + statusBarView = new StatusBarView() + + atom.views.addViewProvider(TestItem, model => { + const element = document.createElement('item-view') + element.model = model + return element + }) + }) + + describe('::addLeftTile({item, priority})', () => { + it('appends the view for the given item to its left side', () => { + const testItem1 = new TestItem(1) + const testItem2 = new TestItem(2) + const testItem3 = new TestItem(3) + + const tile1 = statusBarView.addLeftTile({item: testItem1, priority: 10}) + const tile2 = statusBarView.addLeftTile({item: testItem2, priority: 30}) + const tile3 = statusBarView.addLeftTile({item: testItem3, priority: 20}) + + const {leftPanel} = statusBarView + + expect(leftPanel.children[0].nodeName).toBe('ITEM-VIEW') + expect(leftPanel.children[1].nodeName).toBe('ITEM-VIEW') + expect(leftPanel.children[2].nodeName).toBe('ITEM-VIEW') + + expect(leftPanel.children[0].model).toBe(testItem1) + expect(leftPanel.children[1].model).toBe(testItem3) + expect(leftPanel.children[2].model).toBe(testItem2) + + expect(statusBarView.getLeftTiles()).toEqual([tile1, tile3, tile2]) + expect(tile1.getPriority()).toBe(10) + expect(tile1.getItem()).toBe(testItem1) + }) + + it('allows the view to be removed', () => { + const testItem = new TestItem(1) + const tile = statusBarView.addLeftTile({item: testItem, priority: 10}) + tile.destroy() + expect(statusBarView.leftPanel.children.length).toBe(0) + + statusBarView.addLeftTile({item: testItem, priority: 9}) + }) + + describe('when no priority is given', () => { + it('appends the item', () => { + const testItem1 = new TestItem(1) + const testItem2 = new TestItem(2) + + statusBarView.addLeftTile({item: testItem1, priority: 1000}) + statusBarView.addLeftTile({item: testItem2}) + + const {leftPanel} = statusBarView + expect(leftPanel.children[0].model).toBe(testItem1) + expect(leftPanel.children[1].model).toBe(testItem2) + }) + }) + }) + + describe('::addRightTile({item, priority})', () => { + it('appends the view for the given item to its right side', () => { + const testItem1 = new TestItem(1) + const testItem2 = new TestItem(2) + const testItem3 = new TestItem(3) + + const tile1 = statusBarView.addRightTile({item: testItem1, priority: 10}) + const tile2 = statusBarView.addRightTile({item: testItem2, priority: 30}) + const tile3 = statusBarView.addRightTile({item: testItem3, priority: 20}) + + const {rightPanel} = statusBarView + + expect(rightPanel.children[0].nodeName).toBe('ITEM-VIEW') + expect(rightPanel.children[1].nodeName).toBe('ITEM-VIEW') + expect(rightPanel.children[2].nodeName).toBe('ITEM-VIEW') + + expect(rightPanel.children[0].model).toBe(testItem2) + expect(rightPanel.children[1].model).toBe(testItem3) + expect(rightPanel.children[2].model).toBe(testItem1) + + expect(statusBarView.getRightTiles()).toEqual([tile2, tile3, tile1]) + expect(tile1.getPriority()).toBe(10) + expect(tile1.getItem()).toBe(testItem1) + }) + + it('allows the view to be removed', () => { + const testItem = new TestItem(1) + const disposable = statusBarView.addRightTile({item: testItem, priority: 10}) + disposable.destroy() + expect(statusBarView.rightPanel.children.length).toBe(0) + + statusBarView.addRightTile({item: testItem, priority: 11}) + }) + + describe('when no priority is given', () => { + it('prepends the item', () => { + const testItem1 = new TestItem(1, {priority: 1000}) + const testItem2 = new TestItem(2) + + statusBarView.addRightTile({item: testItem1, priority: 1000}) + statusBarView.addRightTile({item: testItem2}) + + const {rightPanel} = statusBarView + expect(rightPanel.children[0].model).toBe(testItem2) + expect(rightPanel.children[1].model).toBe(testItem1) + }) + }) + }) +})