diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..b60bb86c --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,40 @@ + + +### Prerequisites + +* [ ] Put an X between the brackets on this line if you have done all of the following: + * Reproduced the problem in Safe Mode: http://flight-manual.atom.io/hacking-atom/sections/debugging/#using-safe-mode + * Followed all applicable steps in the debugging guide: http://flight-manual.atom.io/hacking-atom/sections/debugging/ + * Checked the FAQs on the message board for common solutions: https://discuss.atom.io/c/faq + * Checked that your issue isn't already filed: https://github.com/issues?utf8=✓&q=is%3Aissue+user%3Aatom + * Checked that there is not already an Atom package that provides the described functionality: https://atom.io/packages + +### Description + +[Description of the issue] + +### Steps to Reproduce + +1. [First Step] +2. [Second Step] +3. [and so on...] + +**Expected behavior:** [What you expect to happen] + +**Actual behavior:** [What actually happens] + +**Reproduces how often:** [What percentage of the time does it reproduce?] + +### Versions + +You can get this information from copy and pasting the output of `atom --version` and `apm --version` from the command line. Also, please include the OS and what version of the OS you're running. + +### Additional Information + +Any additional information, configuration or data that might be necessary to reproduce the issue. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..cdaa94a8 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,28 @@ +### Requirements + +* Filling out the template is required. Any pull request that does not include enough information to be reviewed in a timely manner may be closed at the maintainers' discretion. +* All new code requires tests to ensure against regressions + +### Description of the Change + + + +### Alternate Designs + + + +### Benefits + + + +### Possible Drawbacks + + + +### Applicable Issues + + diff --git a/README.md b/README.md index 575770ff..ea07ab34 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,3 @@ The Tree View displays icons next to files. These icons are customizable by inst The `atom.file-icons` service must provide the following methods: * `iconClassForPath(path)` - Returns a CSS class name to add to the file view -* `onWillDeactivate` - An event that lets the tree view return to its default icon strategy diff --git a/appveyor.yml b/appveyor.yml index 2b0fde43..32ca55a3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ environment: - ATOM_CHANNEL: beta install: - - ps: Install-Product node 4 + - ps: Install-Product node 6 build_script: - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/atom/ci/master/build-package.ps1')) diff --git a/keymaps/tree-view.cson b/keymaps/tree-view.cson index 08c943c0..98828de7 100644 --- a/keymaps/tree-view.cson +++ b/keymaps/tree-view.cson @@ -68,6 +68,7 @@ 'alt-left': 'tree-view:recursive-collapse-directory' 'h': 'tree-view:collapse-directory' 'enter': 'tree-view:open-selected-entry' + 'escape': 'tree-view:unfocus' 'ctrl-C': 'tree-view:copy-full-path' 'm': 'tree-view:move' 'f2': 'tree-view:move' diff --git a/lib/add-dialog.coffee b/lib/add-dialog.coffee index d29ad748..c8fdbb12 100644 --- a/lib/add-dialog.coffee +++ b/lib/add-dialog.coffee @@ -23,6 +23,12 @@ class AddDialog extends Dialog select: false iconClass: if isCreatingFile then 'icon-file-add' else 'icon-file-directory-create' + onDidCreateFile: (callback) -> + @emitter.on('did-create-file', callback) + + onDidCreateDirectory: (callback) -> + @emitter.on('did-create-directory', callback) + onConfirm: (newPath) -> newPath = newPath.replace(/\s+$/, '') # Remove trailing whitespace endsWithDirectorySeparator = newPath[newPath.length - 1] is path.sep @@ -44,11 +50,11 @@ class AddDialog extends Dialog else fs.writeFileSync(newPath, '') repoForPath(newPath)?.getPathStatus(newPath) - @trigger 'file-created', [newPath] + @emitter.emit('did-create-file', newPath) @close() else fs.makeTreeSync(newPath) - @trigger 'directory-created', [newPath] + @emitter.emit('did-create-directory', newPath) @cancel() catch error @showError("#{error.message}.") diff --git a/lib/copy-dialog.coffee b/lib/copy-dialog.coffee index d80863f2..aa510d84 100644 --- a/lib/copy-dialog.coffee +++ b/lib/copy-dialog.coffee @@ -5,7 +5,7 @@ Dialog = require './dialog' module.exports = class CopyDialog extends Dialog - constructor: (@initialPath) -> + constructor: (@initialPath, {@onCopy}) -> super prompt: 'Enter the new path for the duplicate.' initialPath: atom.project.relativize(@initialPath) @@ -32,8 +32,10 @@ class CopyDialog extends Dialog try if fs.isDirectorySync(@initialPath) fs.copySync(@initialPath, newPath) + @onCopy?({initialPath: @initialPath, newPath: newPath}) else - fs.copy @initialPath, newPath, -> + fs.copy @initialPath, newPath, => + @onCopy?({initialPath: @initialPath, newPath: newPath}) atom.workspace.open newPath, activatePane: true initialLine: activeEditor?.getLastCursor().getBufferRow() diff --git a/lib/dialog.coffee b/lib/dialog.coffee index 715d3c83..24f2e014 100644 --- a/lib/dialog.coffee +++ b/lib/dialog.coffee @@ -1,48 +1,71 @@ -{$, TextEditorView, View} = require 'atom-space-pen-views' +{TextEditor, CompositeDisposable, Disposable, Emitter, Range, Point} = require 'atom' path = require 'path' +{getFullExtension} = require "./helpers" module.exports = -class Dialog extends View - @content: ({prompt} = {}) -> - @div class: 'tree-view-dialog', => - @label prompt, class: 'icon', outlet: 'promptText' - @subview 'miniEditor', new TextEditorView(mini: true) - @div class: 'error-message', outlet: 'errorMessage' - - initialize: ({initialPath, select, iconClass} = {}) -> - @promptText.addClass(iconClass) if iconClass +class Dialog + constructor: ({initialPath, select, iconClass, prompt} = {}) -> + @emitter = new Emitter() + @disposables = new CompositeDisposable() + + @element = document.createElement('div') + @element.classList.add('tree-view-dialog') + + @promptText = document.createElement('label') + @promptText.classList.add('icon') + @promptText.classList.add(iconClass) if iconClass + @promptText.textContent = prompt + @element.appendChild(@promptText) + + @miniEditor = new TextEditor({mini: true}) + blurHandler = => + @close() if document.hasFocus() + @miniEditor.element.addEventListener('blur', blurHandler) + @disposables.add(new Disposable(=> @miniEditor.element.removeEventListener('blur', blurHandler))) + @disposables.add(@miniEditor.onDidChange => @showError()) + @element.appendChild(@miniEditor.element) + + @errorMessage = document.createElement('div') + @errorMessage.classList.add('error-message') + @element.appendChild(@errorMessage) + atom.commands.add @element, 'core:confirm': => @onConfirm(@miniEditor.getText()) 'core:cancel': => @cancel() - @miniEditor.on 'blur', => @close() if document.hasFocus() - @miniEditor.getModel().onDidChange => @showError() - @miniEditor.getModel().setText(initialPath) + + @miniEditor.setText(initialPath) if select - extension = path.extname(initialPath) + extension = getFullExtension(initialPath) baseName = path.basename(initialPath) + selectionStart = initialPath.length - baseName.length if baseName is extension selectionEnd = initialPath.length else selectionEnd = initialPath.length - extension.length - range = [[0, initialPath.length - baseName.length], [0, selectionEnd]] - @miniEditor.getModel().setSelectedBufferRange(range) + @miniEditor.setSelectedBufferRange(Range(Point(0, selectionStart), Point(0, selectionEnd))) attach: -> - @panel = atom.workspace.addModalPanel(item: this.element) - @miniEditor.focus() - @miniEditor.getModel().scrollToCursorPosition() + @panel = atom.workspace.addModalPanel(item: this) + @miniEditor.element.focus() + @miniEditor.scrollToCursorPosition() close: -> - panelToDestroy = @panel + panel = @panel @panel = null - panelToDestroy?.destroy() - atom.workspace.getActivePane().activate() + panel?.destroy() + @emitter.dispose() + @disposables.dispose() + @miniEditor.destroy() + activePane = atom.workspace.getCenter().getActivePane() + activePane.activate() unless activePane.isDestroyed() cancel: -> @close() - $('.tree-view').focus() + document.querySelector('.tree-view')?.focus() showError: (message='') -> - @errorMessage.text(message) - @flashError() if message + @errorMessage.textContent = message + if message + @element.classList.add('error') + window.setTimeout((=> @element.classList.remove('error')), 300) diff --git a/lib/directory-view.coffee b/lib/directory-view.coffee index 784c64f2..45b096f1 100644 --- a/lib/directory-view.coffee +++ b/lib/directory-view.coffee @@ -3,13 +3,16 @@ Directory = require './directory' FileView = require './file-view' {repoForPath} = require './helpers' -class DirectoryView extends HTMLElement - initialize: (@directory) -> +module.exports = +class DirectoryView + constructor: (@directory) -> @subscriptions = new CompositeDisposable() @subscriptions.add @directory.onDidDestroy => @subscriptions.dispose() @subscribeToDirectory() - @classList.add('directory', 'entry', 'list-nested-item', 'collapsed') + @element = document.createElement('li') + @element.setAttribute('is', 'tree-view-directory') + @element.classList.add('directory', 'entry', 'list-nested-item', 'collapsed') @header = document.createElement('div') @header.classList.add('header', 'list-item') @@ -29,36 +32,51 @@ class DirectoryView extends HTMLElement else iconClass = 'icon-file-submodule' if @directory.submodule @directoryName.classList.add(iconClass) - @directoryName.dataset.name = @directory.name - @directoryName.title = @directory.name @directoryName.dataset.path = @directory.path - if @directory.squashedName? - @squashedDirectoryName = document.createElement('span') - @squashedDirectoryName.classList.add('squashed-dir') - @squashedDirectoryName.textContent = @directory.squashedName - - directoryNameTextNode = document.createTextNode(@directory.name) + if @directory.squashedNames? + @directoryName.dataset.name = @directory.squashedNames.join('') + @directoryName.title = @directory.squashedNames.join('') + squashedDirectoryNameNode = document.createElement('span') + squashedDirectoryNameNode.classList.add('squashed-dir') + squashedDirectoryNameNode.textContent = @directory.squashedNames[0] + @directoryName.appendChild(squashedDirectoryNameNode) + @directoryName.appendChild(document.createTextNode(@directory.squashedNames[1])) + else + @directoryName.dataset.name = @directory.name + @directoryName.title = @directory.name + @directoryName.textContent = @directory.name - @appendChild(@header) - if @squashedDirectoryName? - @directoryName.appendChild(@squashedDirectoryName) - @directoryName.appendChild(directoryNameTextNode) + @element.appendChild(@header) @header.appendChild(@directoryName) - @appendChild(@entries) + @element.appendChild(@entries) if @directory.isRoot - @classList.add('project-root') + @element.classList.add('project-root') + @header.classList.add('project-root-header') else - @draggable = true + @element.draggable = true @subscriptions.add @directory.onDidStatusChange => @updateStatus() @updateStatus() @expand() if @directory.expansionState.isExpanded + @element.collapse = @collapse.bind(this) + @element.expand = @expand.bind(this) + @element.toggleExpansion = @toggleExpansion.bind(this) + @element.reload = @reload.bind(this) + @element.isExpanded = @isExpanded + @element.updateStatus = @updateStatus.bind(this) + @element.isPathEqual = @isPathEqual.bind(this) + @element.getPath = @getPath.bind(this) + @element.directory = @directory + @element.header = @header + @element.entries = @entries + @element.directoryName = @directoryName + updateStatus: -> - @classList.remove('status-ignored', 'status-modified', 'status-added') - @classList.add("status-#{@directory.status}") if @directory.status? + @element.classList.remove('status-ignored', 'status-modified', 'status-added') + @element.classList.add("status-#{@directory.status}") if @directory.status? subscribeToDirectory: -> @subscriptions.add @directory.onDidAddEntries (addedEntries) => @@ -71,9 +89,9 @@ class DirectoryView extends HTMLElement insertionIndex = entry.indexInParentDirectory if insertionIndex < numberOfEntries - @entries.insertBefore(view, @entries.children[insertionIndex]) + @entries.insertBefore(view.element, @entries.children[insertionIndex]) else - @entries.appendChild(view) + @entries.appendChild(view.element) numberOfEntries++ @@ -85,16 +103,14 @@ class DirectoryView extends HTMLElement createViewForEntry: (entry) -> if entry instanceof Directory - view = new DirectoryElement() + view = new DirectoryView(entry) else - view = new FileView() - view.initialize(entry) + view = new FileView(entry) subscription = @directory.onDidRemoveEntries (removedEntries) -> - for removedName, removedEntry of removedEntries when entry is removedEntry - view.remove() + if removedEntries.has(entry) + view.element.remove() subscription.dispose() - break @subscriptions.add(subscription) view @@ -108,27 +124,26 @@ class DirectoryView extends HTMLElement expand: (isRecursive=false) -> unless @isExpanded @isExpanded = true - @classList.add('expanded') - @classList.remove('collapsed') + @element.isExpanded = @isExpanded + @element.classList.add('expanded') + @element.classList.remove('collapsed') @directory.expand() if isRecursive - for entry in @entries.children when entry instanceof DirectoryView + for entry in @entries.children when entry.classList.contains('directory') entry.expand(true) false collapse: (isRecursive=false) -> @isExpanded = false + @element.isExpanded = false if isRecursive for entry in @entries.children when entry.isExpanded entry.collapse(true) - @classList.remove('expanded') - @classList.add('collapsed') + @element.classList.remove('expanded') + @element.classList.add('collapsed') @directory.collapse() @entries.innerHTML = '' - -DirectoryElement = document.registerElement('tree-view-directory', prototype: DirectoryView.prototype, extends: 'li') -module.exports = DirectoryElement diff --git a/lib/directory.coffee b/lib/directory.coffee index 279b64b1..329601d8 100644 --- a/lib/directory.coffee +++ b/lib/directory.coffee @@ -9,12 +9,12 @@ realpathCache = {} module.exports = class Directory - constructor: ({@name, fullPath, @symlink, @expansionState, @isRoot, @ignoredPatterns, @useSyncFS}) -> + constructor: ({@name, fullPath, @symlink, @expansionState, @isRoot, @ignoredPatterns, @useSyncFS, @stats}) -> @destroyed = false @emitter = new Emitter() @subscriptions = new CompositeDisposable() - if atom.config.get('tree-view.squashDirectoryNames') + if atom.config.get('tree-view.squashDirectoryNames') and not @isRoot fullPath = @squashDirectoryNames(fullPath) @path = fullPath @@ -26,9 +26,23 @@ class Directory @isRoot ?= false @expansionState ?= {} @expansionState.isExpanded ?= false - @expansionState.entries ?= {} + + # TODO: This can be removed after a sufficient amount + # of time has passed since @expansionState.entries + # has been converted to a Map + unless @expansionState.entries instanceof Map + convertEntriesToMap = (entries) -> + temp = new Map() + for name, entry of entries + entry.entries = convertEntriesToMap(entry.entries) if entry.entries? + temp.set(name, entry) + return temp + + @expansionState.entries = convertEntriesToMap(@expansionState.entries) + + @expansionState.entries ?= new Map() @status = null - @entries = {} + @entries = new Map() @submodule = repoForPath(@path)?.isSubmodule(@path) @@ -54,6 +68,12 @@ class Directory onDidRemoveEntries: (callback) -> @emitter.on('did-remove-entries', callback) + onDidCollapse: (callback) -> + @emitter.on('did-collapse', callback) + + onDidExpand: (callback) -> + @emitter.on('did-expand', callback) + loadRealPath: -> if @useSyncFS @realPath = fs.realpathSync(@path) @@ -148,9 +168,9 @@ class Directory @watchSubscription.close() @watchSubscription = null - for key, entry of @entries + @entries.forEach (entry, key) => entry.destroy() - delete @entries[key] + @entries.delete(key) # Public: Watch this directory for changes. watch: -> @@ -177,22 +197,25 @@ class Directory stat = fs.lstatSyncNoException(fullPath) symlink = stat.isSymbolicLink?() stat = fs.statSyncNoException(fullPath) if symlink + statFlat = _.pick stat, _.keys(stat)... + for key in ["atime", "birthtime", "ctime", "mtime"] + statFlat[key] = statFlat[key]?.getTime() if stat.isDirectory?() - if @entries.hasOwnProperty(name) + if @entries.has(name) # push a placeholder since this entry already exists but this helps # track the insertion index for the created views directories.push(name) else - expansionState = @expansionState.entries[name] - directories.push(new Directory({name, fullPath, symlink, expansionState, @ignoredPatterns, @useSyncFS})) + expansionState = @expansionState.entries.get(name) + directories.push(new Directory({name, fullPath, symlink, expansionState, @ignoredPatterns, @useSyncFS, stats: statFlat})) else if stat.isFile?() - if @entries.hasOwnProperty(name) + if @entries.has(name) # push a placeholder since this entry already exists but this helps # track the insertion index for the created views files.push(name) else - files.push(new File({name, fullPath, symlink, realpathCache, @useSyncFS})) + files.push(new File({name, fullPath, symlink, realpathCache, @useSyncFS, stats: statFlat})) @sortEntries(directories.concat(files)) @@ -216,12 +239,12 @@ class Directory # Public: Perform a synchronous reload of the directory. reload: -> newEntries = [] - removedEntries = _.clone(@entries) + removedEntries = new Map(@entries) index = 0 for entry in @getEntries() - if @entries.hasOwnProperty(entry) - delete removedEntries[entry] + if @entries.has(entry) + removedEntries.delete(entry) index++ continue @@ -230,20 +253,21 @@ class Directory newEntries.push(entry) entriesRemoved = false - for name, entry of removedEntries + removedEntries.forEach (entry, name) => entriesRemoved = true entry.destroy() - if @entries.hasOwnProperty(name) - delete @entries[name] + if @entries.has(name) + @entries.delete(name) - if @expansionState.entries.hasOwnProperty(name) - delete @expansionState.entries[name] + if @expansionState.entries.has(name) + @expansionState.entries.delete(name) - @emitter.emit('did-remove-entries', removedEntries) if entriesRemoved + # Convert removedEntries to a Set containing only the entries for O(1) lookup + @emitter.emit('did-remove-entries', new Set(removedEntries.values())) if entriesRemoved if newEntries.length > 0 - @entries[entry.name] = entry for entry in newEntries + @entries.set(entry.name, entry) for entry in newEntries @emitter.emit('did-add-entries', newEntries) # Public: Collapse this directory and stop watching it. @@ -251,6 +275,7 @@ class Directory @expansionState.isExpanded = false @expansionState = @serializeExpansionState() @unwatch() + @emitter.emit('did-collapse') # Public: Expand this directory, load its children, and start watching it for # changes. @@ -258,19 +283,24 @@ class Directory @expansionState.isExpanded = true @reload() @watch() + @emitter.emit('did-expand') serializeExpansionState: -> expansionState = {} expansionState.isExpanded = @expansionState.isExpanded - expansionState.entries = {} - for name, entry of @entries when entry.expansionState? - expansionState.entries[name] = entry.serializeExpansionState() + expansionState.entries = new Map() + @entries.forEach (entry, name) -> + return unless entry.expansionState? + expansionState.entries.set(name, entry.serializeExpansionState()) expansionState squashDirectoryNames: (fullPath) -> squashedDirs = [@name] loop - contents = fs.listSync fullPath + try + contents = fs.listSync fullPath + catch error + break break if contents.length isnt 1 break if not fs.isDirectorySync(contents[0]) relativeDir = path.relative(fullPath, contents[0]) @@ -278,7 +308,6 @@ class Directory fullPath = path.join(fullPath, relativeDir) if squashedDirs.length > 1 - @squashedName = squashedDirs[0..squashedDirs.length - 2].join(path.sep) + path.sep - @name = squashedDirs[squashedDirs.length - 1] + @squashedNames = [squashedDirs[0..squashedDirs.length - 2].join(path.sep) + path.sep, _.last(squashedDirs)] return fullPath diff --git a/lib/file-view.coffee b/lib/file-view.coffee index dc65a1c9..0a729def 100644 --- a/lib/file-view.coffee +++ b/lib/file-view.coffee @@ -2,36 +2,45 @@ FileIcons = require './file-icons' module.exports = -class FileView extends HTMLElement - initialize: (@file) -> +class FileView + constructor: (@file) -> @subscriptions = new CompositeDisposable() @subscriptions.add @file.onDidDestroy => @subscriptions.dispose() - @draggable = true - - @classList.add('file', 'entry', 'list-item') + @element = document.createElement('li') + @element.setAttribute('is', 'tree-view-file') + @element.draggable = true + @element.classList.add('file', 'entry', 'list-item') @fileName = document.createElement('span') @fileName.classList.add('name', 'icon') - @appendChild(@fileName) + @element.appendChild(@fileName) @fileName.textContent = @file.name @fileName.title = @file.name @fileName.dataset.name = @file.name @fileName.dataset.path = @file.path - @fileName.classList.add(FileIcons.getService().iconClassForPath(@file.path)) + iconClass = FileIcons.getService().iconClassForPath(@file.path, "tree-view") + if iconClass + unless Array.isArray iconClass + iconClass = iconClass.toString().split(/\s+/g) + @fileName.classList.add(iconClass...) @subscriptions.add @file.onDidStatusChange => @updateStatus() @updateStatus() + @element.getPath = @getPath.bind(this) + @element.isPathEqual = @isPathEqual.bind(this) + @element.file = @file + @element.fileName = @fileName + @element.updateStatus = @updateStatus.bind(this) + updateStatus: -> - @classList.remove('status-ignored', 'status-modified', 'status-added') - @classList.add("status-#{@file.status}") if @file.status? + @element.classList.remove('status-ignored', 'status-modified', 'status-added') + @element.classList.add("status-#{@file.status}") if @file.status? getPath: -> @fileName.dataset.path isPathEqual: (pathToCompare) -> @file.isPathEqual(pathToCompare) - -module.exports = document.registerElement('tree-view-file', prototype: FileView.prototype, extends: 'li') diff --git a/lib/file.coffee b/lib/file.coffee index de898b06..902c4380 100644 --- a/lib/file.coffee +++ b/lib/file.coffee @@ -5,7 +5,7 @@ fs = require 'fs-plus' module.exports = class File - constructor: ({@name, fullPath, @symlink, realpathCache, useSyncFS}) -> + constructor: ({@name, fullPath, @symlink, realpathCache, useSyncFS, @stats}) -> @destroyed = false @emitter = new Emitter() @subscriptions = new CompositeDisposable() @@ -36,7 +36,7 @@ class File onDidStatusChange: (callback) -> @emitter.on('did-status-change', callback) - # Subscribe to the project' repo for changes to the Git status of this file. + # Subscribe to the project's repo for changes to the Git status of this file. subscribeToRepo: -> repo = repoForPath(@path) return unless repo? diff --git a/lib/helpers.coffee b/lib/helpers.coffee index c67e0d97..6f31695d 100644 --- a/lib/helpers.coffee +++ b/lib/helpers.coffee @@ -22,3 +22,10 @@ module.exports = fullExtension = extension + fullExtension filePath = path.basename(filePath, extension) fullExtension + + updateEditorsForPath: (oldPath, newPath) -> + editors = atom.workspace.getTextEditors() + for editor in editors + filePath = editor.getPath() + if filePath?.startsWith(oldPath) + editor.getBuffer().setPath(filePath.replace(oldPath, newPath)) diff --git a/lib/main.coffee b/lib/main.coffee deleted file mode 100644 index 9d2eb4a3..00000000 --- a/lib/main.coffee +++ /dev/null @@ -1,63 +0,0 @@ -{CompositeDisposable} = require 'event-kit' -path = require 'path' - -FileIcons = require './file-icons' - -module.exports = - treeView: null - - activate: (@state) -> - @disposables = new CompositeDisposable - @state.attached ?= true if @shouldAttach() - - @createView() if @state.attached - - @disposables.add atom.commands.add('atom-workspace', { - 'tree-view:show': => @createView().show() - 'tree-view:toggle': => @createView().toggle() - 'tree-view:toggle-focus': => @createView().toggleFocus() - 'tree-view:reveal-active-file': => @createView().revealActiveFile() - 'tree-view:toggle-side': => @createView().toggleSide() - 'tree-view:add-file': => @createView().add(true) - 'tree-view:add-folder': => @createView().add(false) - 'tree-view:duplicate': => @createView().copySelectedEntry() - 'tree-view:remove': => @createView().removeSelectedEntries() - 'tree-view:rename': => @createView().moveSelectedEntry() - }) - - deactivate: -> - @disposables.dispose() - @fileIconsDisposable?.dispose() - @treeView?.deactivate() - @treeView = null - - consumeFileIcons: (service) -> - FileIcons.setService(service) - @fileIconsDisposable = service.onWillDeactivate -> - FileIcons.resetService() - @treeView?.updateRoots() - @treeView?.updateRoots() - - serialize: -> - if @treeView? - @treeView.serialize() - else - @state - - createView: -> - unless @treeView? - TreeView = require './tree-view' - @treeView = new TreeView(@state) - @treeView - - shouldAttach: -> - projectPath = atom.project.getPaths()[0] - if atom.workspace.getActivePaneItem() - false - else if path.basename(projectPath) is '.git' - # Only attach when the project path matches the path to open signifying - # the .git folder was opened explicitly and not by using Atom as the Git - # editor. - projectPath is atom.getLoadSettings().pathToOpen - else - true diff --git a/lib/main.js b/lib/main.js new file mode 100644 index 00000000..88b8d199 --- /dev/null +++ b/lib/main.js @@ -0,0 +1,3 @@ +const TreeViewPackage = require('./tree-view-package') + +module.exports = new TreeViewPackage() diff --git a/lib/move-dialog.coffee b/lib/move-dialog.coffee index 69d50d8e..e10d5c85 100644 --- a/lib/move-dialog.coffee +++ b/lib/move-dialog.coffee @@ -5,7 +5,7 @@ Dialog = require './dialog' module.exports = class MoveDialog extends Dialog - constructor: (@initialPath) -> + constructor: (@initialPath, {@onMove}) -> if fs.isDirectorySync(@initialPath) prompt = 'Enter the new path for the directory.' else @@ -36,6 +36,7 @@ class MoveDialog extends Dialog try fs.makeTreeSync(directoryPath) unless fs.existsSync(directoryPath) fs.moveSync(@initialPath, newPath) + @onMove?(initialPath: @initialPath, newPath: newPath) if repo = repoForPath(newPath) repo.getPathStatus(@initialPath) repo.getPathStatus(newPath) diff --git a/lib/root-drag-and-drop.coffee b/lib/root-drag-and-drop.coffee new file mode 100644 index 00000000..afdfb2c3 --- /dev/null +++ b/lib/root-drag-and-drop.coffee @@ -0,0 +1,201 @@ +url = require 'url' + +{ipcRenderer, remote} = require 'electron' + +_ = require 'underscore-plus' + +module.exports = +class RootDragAndDropHandler + constructor: (@treeView) -> + ipcRenderer.on('tree-view:project-folder-dropped', @onDropOnOtherWindow) + @handleEvents() + + dispose: -> + ipcRenderer.removeListener('tree-view:project-folder-dropped', @onDropOnOtherWindow) + + handleEvents: -> + # onDragStart is called directly by TreeView's onDragStart + # will be cleaned up by tree view, since they are tree-view's handlers + @treeView.element.addEventListener 'dragenter', @onDragEnter.bind(this) + @treeView.element.addEventListener 'dragend', @onDragEnd.bind(this) + @treeView.element.addEventListener 'dragleave', @onDragLeave.bind(this) + @treeView.element.addEventListener 'dragover', @onDragOver.bind(this) + @treeView.element.addEventListener 'drop', @onDrop.bind(this) + + onDragStart: (e) => + return unless @treeView.list.contains(e.target) + + @prevDropTargetIndex = null + e.dataTransfer.setData 'atom-tree-view-event', 'true' + projectRoot = e.target.closest('.project-root') + directory = projectRoot.directory + + e.dataTransfer.setData 'project-root-index', Array.from(projectRoot.parentElement.children).indexOf(projectRoot) + + rootIndex = -1 + (rootIndex = index; break) for root, index in @treeView.roots when root.directory is directory + + e.dataTransfer.setData 'from-root-index', rootIndex + e.dataTransfer.setData 'from-root-path', directory.path + e.dataTransfer.setData 'from-window-id', @getWindowId() + + e.dataTransfer.setData 'text/plain', directory.path + + if process.platform in ['darwin', 'linux'] + pathUri = "file://#{directory.path}" unless @uriHasProtocol(directory.path) + e.dataTransfer.setData 'text/uri-list', pathUri + + uriHasProtocol: (uri) -> + try + url.parse(uri).protocol? + catch error + false + + onDragEnter: (e) -> + return unless @treeView.list.contains(e.target) + + e.stopPropagation() + + onDragLeave: (e) => + return unless @treeView.list.contains(e.target) + + e.stopPropagation() + @removePlaceholder() if e.target is e.currentTarget + + onDragEnd: (e) => + return unless e.target.matches('.project-root-header') + + e.stopPropagation() + @clearDropTarget() + + onDragOver: (e) => + return unless @treeView.list.contains(e.target) + + unless @isAtomTreeViewEvent(e) + return + + e.preventDefault() + e.stopPropagation() + + entry = e.currentTarget + + if @treeView.roots.length is 0 + @treeView.list.appendChild(@getPlaceholder()) + return + + newDropTargetIndex = @getDropTargetIndex(e) + return unless newDropTargetIndex? + return if @prevDropTargetIndex is newDropTargetIndex + @prevDropTargetIndex = newDropTargetIndex + + projectRoots = @treeView.roots + + if newDropTargetIndex < projectRoots.length + element = projectRoots[newDropTargetIndex] + element.classList.add('is-drop-target') + element.parentElement.insertBefore(@getPlaceholder(), element) + else + element = projectRoots[newDropTargetIndex - 1] + element.classList.add('drop-target-is-after') + element.parentElement.insertBefore(@getPlaceholder(), element.nextSibling) + + onDropOnOtherWindow: (e, fromItemIndex) => + paths = atom.project.getPaths() + paths.splice(fromItemIndex, 1) + atom.project.setPaths(paths) + + @clearDropTarget() + + clearDropTarget: -> + element = @treeView.element.querySelector(".is-dragging") + element?.classList.remove('is-dragging') + element?.updateTooltip() + @removePlaceholder() + + onDrop: (e) => + return unless @treeView.list.contains(e.target) + + e.preventDefault() + e.stopPropagation() + + {dataTransfer} = e + + # TODO: support dragging folders from the filesystem -- electron needs to add support first + return unless @isAtomTreeViewEvent(e) + + fromWindowId = parseInt(dataTransfer.getData('from-window-id')) + fromRootPath = dataTransfer.getData('from-root-path') + fromIndex = parseInt(dataTransfer.getData('project-root-index')) + fromRootIndex = parseInt(dataTransfer.getData('from-root-index')) + + toIndex = @getDropTargetIndex(e) + + @clearDropTarget() + + if fromWindowId is @getWindowId() + unless fromIndex is toIndex + projectPaths = atom.project.getPaths() + projectPaths.splice(fromIndex, 1) + if toIndex > fromIndex then toIndex -= 1 + projectPaths.splice(toIndex, 0, fromRootPath) + atom.project.setPaths(projectPaths) + else + projectPaths = atom.project.getPaths() + projectPaths.splice(toIndex, 0, fromRootPath) + atom.project.setPaths(projectPaths) + + if not isNaN(fromWindowId) + # Let the window where the drag started know that the tab was dropped + browserWindow = remote.BrowserWindow.fromId(fromWindowId) + browserWindow?.webContents.send('tree-view:project-folder-dropped', fromIndex) + + getDropTargetIndex: (e) -> + return if @isPlaceholder(e.target) + + projectRoots = @treeView.roots + projectRoot = e.target.closest('.project-root') + projectRoot = projectRoots[projectRoots.length - 1] unless projectRoot + + return 0 unless projectRoot + + projectRootIndex = @treeView.roots.indexOf(projectRoot) + + center = projectRoot.getBoundingClientRect().top + projectRoot.offsetHeight / 2 + + if e.pageY < center + projectRootIndex + else + projectRootIndex + 1 + + canDragStart: (e) -> + e.target.closest('.project-root-header') + + isDragging: (e) -> + for item in e.dataTransfer.items + if item.type is 'from-root-path' + return true + + return false + + isAtomTreeViewEvent: (e) -> + for item in e.dataTransfer.items + if item.type is 'atom-tree-view-event' + return true + + return false + + getPlaceholder: -> + unless @placeholderEl + @placeholderEl = document.createElement('li') + @placeholderEl.classList.add('placeholder') + @placeholderEl + + removePlaceholder: -> + @placeholderEl?.remove() + @placeholderEl = null + + isPlaceholder: (element) -> + element.classList.contains('.placeholder') + + getWindowId: -> + @processId ?= atom.getCurrentWindow().id diff --git a/lib/tree-view-package.js b/lib/tree-view-package.js new file mode 100644 index 00000000..9b92ef41 --- /dev/null +++ b/lib/tree-view-package.js @@ -0,0 +1,101 @@ +const {Disposable, CompositeDisposable} = require('event-kit') +const path = require('path') + +const FileIcons = require('./file-icons') +const TreeView = require('./tree-view') + +module.exports = +class TreeViewPackage { + activate () { + this.disposables = new CompositeDisposable() + this.disposables.add(atom.commands.add('atom-workspace', { + 'tree-view:show': () => this.getTreeViewInstance().show(), + 'tree-view:toggle': () => this.getTreeViewInstance().toggle(), + 'tree-view:toggle-focus': () => this.getTreeViewInstance().toggleFocus(), + 'tree-view:reveal-active-file': () => this.getTreeViewInstance().revealActiveFile(), + 'tree-view:add-file': () => this.getTreeViewInstance().add(true), + 'tree-view:add-folder': () => this.getTreeViewInstance().add(false), + 'tree-view:duplicate': () => this.getTreeViewInstance().copySelectedEntry(), + 'tree-view:remove': () => this.getTreeViewInstance().removeSelectedEntries(), + 'tree-view:rename': () => this.getTreeViewInstance().moveSelectedEntry(), + 'tree-view:show-current-file-in-file-manager': () => this.getTreeViewInstance().showCurrentFileInFileManager() + })) + + this.disposables.add(atom.project.onDidChangePaths(this.createOrDestroyTreeViewIfNeeded.bind(this))) + + if (this.shouldAttachTreeView()) { + const treeView = this.getTreeViewInstance() + const showOnAttach = !atom.workspace.getActivePaneItem() + this.treeViewOpenPromise = atom.workspace.open(treeView, { + activatePane: showOnAttach, + activateItem: showOnAttach + }) + } else { + this.treeViewOpenPromise = Promise.resolve() + } + } + + deactivate () { + this.disposables.dispose() + if (this.fileIconsDisposable) this.fileIconsDisposable.dispose() + if (this.treeView) this.treeView.destroy() + this.treeView = null + } + + consumeFileIcons (service) { + FileIcons.setService(service) + if (this.treeView) this.treeView.updateRoots() + return new Disposable(() => { + FileIcons.resetService() + if (this.treeView) this.treeView.updateRoots() + } + ) + } + + getTreeViewInstance (state = {}) { + if (this.treeView == null) { + this.treeView = new TreeView(state) + this.treeView.onDidDestroy(() => this.treeView = null) + } + return this.treeView + } + + createOrDestroyTreeViewIfNeeded () { + if (this.shouldAttachTreeView()) { + const treeView = this.getTreeViewInstance() + const paneContainer = atom.workspace.paneContainerForURI(treeView.getURI()) + if (paneContainer) { + paneContainer.show() + } else { + atom.workspace.open(treeView, { + activatePane: false, + activateItem: false + }).then(() => { + const paneContainer = atom.workspace.paneContainerForURI(treeView.getURI()) + if (paneContainer) paneContainer.show() + }) + } + } else { + if (this.treeView) { + const pane = atom.workspace.paneForItem(this.treeView) + if (pane) pane.removeItem(this.treeView) + } + } + } + + shouldAttachTreeView () { + if (atom.project.getPaths().length === 0) return false + + // Avoid opening the tree view if Atom was opened as the Git editor... + // Only show it if the .git folder was explicitly opened. + if (path.basename(atom.project.getPaths()[0]) === '.git') { + return atom.project.getPaths()[0] === atom.getLoadSettings().pathToOpen + } + + return true + } + + shouldShowTreeViewAfterAttaching () { + if (atom.workspace.getActivePaneItem()) return false + } +} diff --git a/lib/tree-view.coffee b/lib/tree-view.coffee index 5abf3031..c218adb7 100644 --- a/lib/tree-view.coffee +++ b/lib/tree-view.coffee @@ -2,46 +2,50 @@ path = require 'path' {shell} = require 'electron' _ = require 'underscore-plus' -{BufferedProcess, CompositeDisposable} = require 'atom' -{repoForPath, getStyleObject, getFullExtension} = require "./helpers" -{$, View} = require 'atom-space-pen-views' +{BufferedProcess, CompositeDisposable, Emitter} = require 'atom' +{repoForPath, getStyleObject, getFullExtension, updateEditorsForPath} = require "./helpers" fs = require 'fs-plus' -AddDialog = null # Defer requiring until actually needed -MoveDialog = null # Defer requiring until actually needed -CopyDialog = null # Defer requiring until actually needed +AddDialog = require './add-dialog' +MoveDialog = require './move-dialog' +CopyDialog = require './copy-dialog' Minimatch = null # Defer requiring until actually needed Directory = require './directory' DirectoryView = require './directory-view' -FileView = require './file-view' -LocalStorage = window.localStorage +RootDragAndDrop = require './root-drag-and-drop' + +TREE_VIEW_URI = 'atom://tree-view' toggleConfig = (keyPath) -> atom.config.set(keyPath, not atom.config.get(keyPath)) +nextId = 1 + module.exports = -class TreeView extends View - panel: null +class TreeView + constructor: (state) -> + @id = nextId++ + @element = document.createElement('div') + @element.classList.add('tool-panel', 'tree-view') + @element.tabIndex = -1 - @content: -> - @div class: 'tree-view-resizer tool-panel', 'data-show-on-right-side': atom.config.get('tree-view.showOnRightSide'), => - @div class: 'tree-view-scroller order--center', outlet: 'scroller', => - @ol class: 'tree-view full-menu list-tree has-collapsable-children focusable-panel', tabindex: -1, outlet: 'list' - @div class: 'tree-view-resize-handle', outlet: 'resizeHandle' + @list = document.createElement('ol') + @list.classList.add('tree-view-root', 'full-menu', 'list-tree', 'has-collapsable-children', 'focusable-panel') + @element.appendChild(@list) - initialize: (state) -> @disposables = new CompositeDisposable - @focusAfterAttach = false + @emitter = new Emitter @roots = [] - @scrollLeftAfterAttach = -1 - @scrollTopAfterAttach = -1 @selectedPath = null + @selectOnMouseUp = null + @lastFocusedElement = null @ignoredPatterns = [] @useSyncFS = false @currentlyOpening = new Map @dragEventCounts = new WeakMap + @rootDragAndDrop = new RootDragAndDrop(this) @handleEvents() @@ -56,53 +60,93 @@ class TreeView extends View @selectEntry(@roots[0]) @selectEntryForPath(state.selectedPath) if state.selectedPath - @focusAfterAttach = state.hasFocus - @scrollTopAfterAttach = state.scrollTop if state.scrollTop - @scrollLeftAfterAttach = state.scrollLeft if state.scrollLeft - @attachAfterProjectPathSet = state.attached and _.isEmpty(atom.project.getPaths()) - @width(state.width) if state.width > 0 - @attach() if state.attached - attached: -> - @focus() if @focusAfterAttach - @scroller.scrollLeft(@scrollLeftAfterAttach) if @scrollLeftAfterAttach > 0 - @scrollTop(@scrollTopAfterAttach) if @scrollTopAfterAttach > 0 + if state.scrollTop? or state.scrollLeft? + observer = new IntersectionObserver(=> + if @isVisible() + @element.scrollTop = state.scrollTop + @element.scrollLeft = state.scrollLeft + observer.disconnect() + ) + observer.observe(@element) + + @element.style.width = "#{state.width}px" if state.width > 0 - detached: -> - @resizeStopped() + @disposables.add @onEntryMoved ({initialPath, newPath}) -> + updateEditorsForPath(initialPath, newPath) + + @disposables.add @onEntryDeleted ({path}) -> + for editor in atom.workspace.getTextEditors() + if editor?.getPath()?.startsWith(path) + editor.destroy() serialize: -> directoryExpansionStates: new ((roots) -> @[root.directory.path] = root.directory.serializeExpansionState() for root in roots this)(@roots) + deserializer: 'TreeView' selectedPath: @selectedEntry()?.getPath() - hasFocus: @hasFocus() - attached: @panel? - scrollLeft: @scroller.scrollLeft() - scrollTop: @scrollTop() - width: @width() + scrollLeft: @element.scrollLeft + scrollTop: @element.scrollTop + width: parseInt(@element.style.width or 0) - deactivate: -> + destroy: -> root.directory.destroy() for root in @roots @disposables.dispose() - @detach() if @panel? + @rootDragAndDrop.dispose() + @emitter.emit('did-destroy') + + onDidDestroy: (callback) -> + @emitter.on('did-destroy', callback) + + getTitle: -> "Project" + + getURI: -> TREE_VIEW_URI + + getPreferredLocation: -> + if atom.config.get('tree-view.showOnRightSide') + 'right' + else + 'left' + + getAllowedLocations: -> ["left", "right"] + + isPermanentDockItem: -> true + + getPreferredWidth: -> + @list.style.width = 'min-content' + result = @list.offsetWidth + @list.style.width = '' + result + + onDirectoryCreated: (callback) -> + @emitter.on('directory-created', callback) + + onEntryCopied: (callback) -> + @emitter.on('entry-copied', callback) + + onEntryDeleted: (callback) -> + @emitter.on('entry-deleted', callback) + + onEntryMoved: (callback) -> + @emitter.on('entry-moved', callback) + + onFileCreated: (callback) -> + @emitter.on('file-created', callback) handleEvents: -> - @on 'dblclick', '.tree-view-resize-handle', => - @resizeToFitContent() - @on 'click', '.entry', (e) => + @element.addEventListener 'click', (e) => # This prevents accidental collapsing when a .entries element is the event target return if e.target.classList.contains('entries') @entryClicked(e) unless e.shiftKey or e.metaKey or e.ctrlKey - @on 'mousedown', '.entry', (e) => - @onMouseDown(e) - @on 'mousedown', '.tree-view-resize-handle', (e) => @resizeStarted(e) - @on 'dragstart', '.entry', (e) => @onDragStart(e) - @on 'dragenter', '.entry.directory > .header', (e) => @onDragEnter(e) - @on 'dragleave', '.entry.directory > .header', (e) => @onDragLeave(e) - @on 'dragover', '.entry', (e) => @onDragOver(e) - @on 'drop', '.entry', (e) => @onDrop(e) + @element.addEventListener 'mousedown', (e) => @onMouseDown(e) + @element.addEventListener 'mouseup', (e) => @onMouseUp(e) + @element.addEventListener 'dragstart', (e) => @onDragStart(e) + @element.addEventListener 'dragenter', (e) => @onDragEnter(e) + @element.addEventListener 'dragleave', (e) => @onDragLeave(e) + @element.addEventListener 'dragover', (e) => @onDragOver(e) + @element.addEventListener 'drop', (e) => @onDrop(e) atom.commands.add @element, 'core:move-up': @moveUp.bind(this) @@ -128,7 +172,7 @@ class TreeView extends View 'tree-view:show-in-file-manager': => @showSelectedEntryInFileManager() 'tree-view:open-in-new-window': => @openSelectedEntryInNewWindow() 'tree-view:copy-project-path': => @copySelectedEntryPath(true) - 'tool-panel:unfocus': => @unfocus() + 'tree-view:unfocus': => @unfocus() 'tree-view:toggle-vcs-ignored-files': -> toggleConfig 'tree-view.hideVcsIgnoredFiles' 'tree-view:toggle-ignored-names': -> toggleConfig 'tree-view.hideIgnoredNames' 'tree-view:remove-project-folder': (e) => @removeProjectFolder(e) @@ -137,9 +181,9 @@ class TreeView extends View atom.commands.add @element, "tree-view:open-selected-entry-in-pane-#{index + 1}", => @openSelectedEntryInPane index - @disposables.add atom.workspace.onDidChangeActivePaneItem => + @disposables.add atom.workspace.getCenter().onDidChangeActivePaneItem => @selectActiveFile() - @revealActiveFile() if atom.config.get('tree-view.autoReveal') + @revealActiveFile(false) if atom.config.get('tree-view.autoReveal') @disposables.add atom.project.onDidChangePaths => @updateRoots() @disposables.add atom.config.onDidChange 'tree-view.hideVcsIgnoredFiles', => @@ -148,73 +192,53 @@ class TreeView extends View @updateRoots() @disposables.add atom.config.onDidChange 'core.ignoredNames', => @updateRoots() if atom.config.get('tree-view.hideIgnoredNames') - @disposables.add atom.config.onDidChange 'tree-view.showOnRightSide', ({newValue}) => - @onSideToggled(newValue) @disposables.add atom.config.onDidChange 'tree-view.sortFoldersBeforeFiles', => @updateRoots() @disposables.add atom.config.onDidChange 'tree-view.squashDirectoryNames', => @updateRoots() toggle: -> - if @isVisible() - @detach() - else - @show() - - show: -> - @attach() - @focus() - - attach: -> - return if _.isEmpty(atom.project.getPaths()) + atom.workspace.toggle(this) - @panel ?= - if atom.config.get('tree-view.showOnRightSide') - atom.workspace.addRightPanel(item: this) - else - atom.workspace.addLeftPanel(item: this) - - detach: -> - @scrollLeftAfterAttach = @scroller.scrollLeft() - @scrollTopAfterAttach = @scrollTop() + show: (focus) -> + atom.workspace.open(this, { + searchAllPanes: true, + activatePane: false, + activateItem: false, + }).then => + atom.workspace.paneContainerForURI(@getURI()).show() + @focus() if focus - # Clean up copy and cut localStorage Variables - LocalStorage['tree-view:cutPath'] = null - LocalStorage['tree-view:copyPath'] = null - - @panel.destroy() - @panel = null - @unfocus() + hide: -> + atom.workspace.hide(this) focus: -> - @list.focus() + @element.focus() unfocus: -> - atom.workspace.getActivePane().activate() + atom.workspace.getCenter().activate() hasFocus: -> - @list.is(':focus') or document.activeElement is @list[0] + document.activeElement is @element toggleFocus: -> if @hasFocus() @unfocus() else - @show() + @show(true) entryClicked: (e) -> - entry = e.currentTarget - isRecursive = e.altKey or false - @selectEntry(entry) - if entry instanceof DirectoryView - entry.toggleExpansion(isRecursive) - else if entry instanceof FileView - @fileViewEntryClicked(e) - - false + if entry = e.target.closest('.entry') + isRecursive = e.altKey or false + @selectEntry(entry) + if entry.classList.contains('directory') + entry.toggleExpansion(isRecursive) + else if entry.classList.contains('file') + @fileViewEntryClicked(e) fileViewEntryClicked: (e) -> - filePath = e.currentTarget.getPath() - detail = e.originalEvent?.detail ? 1 + filePath = e.target.closest('.entry').getPath() + detail = e.detail ? 1 alwaysOpenExisting = atom.config.get('tree-view.alwaysOpenExisting') if detail is 1 if atom.config.get('core.allowPendingPaneItems') @@ -226,31 +250,10 @@ class TreeView extends View openAfterPromise: (uri, options) -> if promise = @currentlyOpening.get(uri) - promise.then => atom.workspace.open(uri, options) + promise.then -> atom.workspace.open(uri, options) else atom.workspace.open(uri, options) - resizeStarted: => - $(document).on('mousemove', @resizeTreeView) - $(document).on('mouseup', @resizeStopped) - - resizeStopped: => - $(document).off('mousemove', @resizeTreeView) - $(document).off('mouseup', @resizeStopped) - - resizeTreeView: ({pageX, which}) => - return @resizeStopped() unless which is 1 - - if atom.config.get('tree-view.showOnRightSide') - width = @outerWidth() + @offset().left - pageX - else - width = pageX - @offset().left - @width(width) - - resizeToFitContent: -> - @width(1) # Shrink to measure the minimum width of list - @width(@list.outerWidth()) - loadIgnoredPatterns: -> @ignoredPatterns.length = 0 return unless atom.config.get('tree-view.hideIgnoredNames') @@ -275,6 +278,12 @@ class TreeView extends View @loadIgnoredPatterns() @roots = for projectPath in atom.project.getPaths() + stats = fs.lstatSyncNoException(projectPath) + continue unless stats + stats = _.pick stats, _.keys(stats)... + for key in ["atime", "birthtime", "ctime", "mtime"] + stats[key] = stats[key].getTime() + directory = new Directory({ name: path.basename(projectPath) fullPath: projectPath @@ -285,45 +294,40 @@ class TreeView extends View {isExpanded: true} @ignoredPatterns @useSyncFS + stats }) - root = new DirectoryView() - root.initialize(directory) - @list[0].appendChild(root) + root = new DirectoryView(directory).element + @list.appendChild(root) root - if @attachAfterProjectPathSet - @attach() - @attachAfterProjectPathSet = false - - getActivePath: -> atom.workspace.getActivePaneItem()?.getPath?() + getActivePath: -> atom.workspace.getCenter().getActivePaneItem()?.getPath?() selectActiveFile: -> if activeFilePath = @getActivePath() @selectEntryForPath(activeFilePath) - else - @deselect() - - revealActiveFile: -> - return if _.isEmpty(atom.project.getPaths()) - @attach() - @focus() if atom.config.get('tree-view.focusOnReveal') - - return unless activeFilePath = @getActivePath() - - [rootPath, relativePath] = atom.project.relativizePath(activeFilePath) - return unless rootPath? - - activePathComponents = relativePath.split(path.sep) - currentPath = rootPath - for pathComponent in activePathComponents - currentPath += path.sep + pathComponent - entry = @entryForPath(currentPath) - if entry instanceof DirectoryView - entry.expand() - else - @selectEntry(entry) - @scrollToEntry(entry) + revealActiveFile: (focus) -> + return Promise.resolve() unless atom.project.getPaths().length + + @show(focus ? atom.config.get('tree-view.focusOnReveal')).then => + return unless activeFilePath = @getActivePath() + + [rootPath, relativePath] = atom.project.relativizePath(activeFilePath) + return unless rootPath? + + activePathComponents = relativePath.split(path.sep) + # Add the root folder to the path components + activePathComponents.unshift(rootPath.substr(rootPath.lastIndexOf(path.sep) + 1)) + # And remove it from the current path + currentPath = rootPath.substr(0, rootPath.lastIndexOf(path.sep)) + for pathComponent in activePathComponents + currentPath += path.sep + pathComponent + entry = @entryForPath(currentPath) + if entry.classList.contains('directory') + entry.expand() + else + @selectEntry(entry) + @scrollToEntry(entry) copySelectedEntryPath: (relativePath = false) -> if pathToCopy = @selectedPath @@ -334,7 +338,7 @@ class TreeView extends View bestMatchEntry = null bestMatchLength = 0 - for entry in @list[0].querySelectorAll('.entry') + for entry in @list.querySelectorAll('.entry') if entry.isPathEqual(entryPath) return entry @@ -352,67 +356,94 @@ class TreeView extends View event?.stopImmediatePropagation() selectedEntry = @selectedEntry() if selectedEntry? - if selectedEntry instanceof DirectoryView + if selectedEntry.classList.contains('directory') if @selectEntry(selectedEntry.entries.children[0]) - @scrollToEntry(@selectedEntry()) + @scrollToEntry(@selectedEntry(), false) return - selectedEntry = $(selectedEntry) - until @selectEntry(selectedEntry.next('.entry')[0]) - selectedEntry = selectedEntry.parents('.entry:first') - break unless selectedEntry.length + if nextEntry = @nextEntry(selectedEntry) + @selectEntry(nextEntry) else @selectEntry(@roots[0]) - @scrollToEntry(@selectedEntry()) + @scrollToEntry(@selectedEntry(), false) moveUp: (event) -> event.stopImmediatePropagation() selectedEntry = @selectedEntry() if selectedEntry? - selectedEntry = $(selectedEntry) - if previousEntry = @selectEntry(selectedEntry.prev('.entry')[0]) - if previousEntry instanceof DirectoryView + if previousEntry = @previousEntry(selectedEntry) + @selectEntry(previousEntry) + if previousEntry.classList.contains('directory') @selectEntry(_.last(previousEntry.entries.children)) else - @selectEntry(selectedEntry.parents('.directory').first()?[0]) + @selectEntry(selectedEntry.parentElement.closest('.directory')) else - @selectEntry(@list.find('.entry').last()?[0]) + entries = @list.querySelectorAll('.entry') + @selectEntry(entries[entries.length - 1]) + + @scrollToEntry(@selectedEntry(), false) + + nextEntry: (entry) -> + currentEntry = entry + while currentEntry? + if currentEntry.nextSibling? + currentEntry = currentEntry.nextSibling + if currentEntry.matches('.entry') + return currentEntry + else + currentEntry = currentEntry.parentElement.closest('.directory') + + return null - @scrollToEntry(@selectedEntry()) + previousEntry: (entry) -> + currentEntry = entry + while currentEntry? + currentEntry = currentEntry.previousSibling + if currentEntry?.matches('.entry') + return currentEntry + return null expandDirectory: (isRecursive=false) -> selectedEntry = @selectedEntry() - if isRecursive is false and selectedEntry.isExpanded - @moveDown() if selectedEntry.directory.getEntries().length > 0 + return unless selectedEntry? + + directory = selectedEntry.closest('.directory') + if isRecursive is false and directory.isExpanded + # Select the first entry in the expanded folder if it exists + @moveDown() if directory.directory.getEntries().length > 0 else - selectedEntry.expand(isRecursive) + directory.expand(isRecursive) collapseDirectory: (isRecursive=false) -> selectedEntry = @selectedEntry() return unless selectedEntry? - if directory = $(selectedEntry).closest('.expanded.directory')[0] + if directory = selectedEntry.closest('.expanded.directory') directory.collapse(isRecursive) @selectEntry(directory) openSelectedEntry: (options={}, expandDirectory=false) -> selectedEntry = @selectedEntry() - if selectedEntry instanceof DirectoryView + return unless selectedEntry? + + if selectedEntry.classList.contains('directory') if expandDirectory @expandDirectory(false) else selectedEntry.toggleExpansion() - else if selectedEntry instanceof FileView + else if selectedEntry.classList.contains('file') if atom.config.get('tree-view.alwaysOpenExisting') options = Object.assign searchAllPanes: true, options @openAfterPromise(selectedEntry.getPath(), options) openSelectedEntrySplit: (orientation, side) -> selectedEntry = @selectedEntry() - pane = atom.workspace.getActivePane() - if pane and selectedEntry instanceof FileView - if atom.workspace.getActivePaneItem() + return unless selectedEntry? + + pane = atom.workspace.getCenter().getActivePane() + if pane and selectedEntry.classList.contains('file') + if atom.workspace.getCenter().getActivePaneItem() split = pane.split orientation, side atom.workspace.openURIInPane selectedEntry.getPath(), split else @@ -432,8 +463,10 @@ class TreeView extends View openSelectedEntryInPane: (index) -> selectedEntry = @selectedEntry() - pane = atom.workspace.getPanes()[index] - if pane and selectedEntry instanceof FileView + return unless selectedEntry? + + pane = atom.workspace.getCenter().getPanes()[index] + if pane and selectedEntry.classList.contains('file') atom.workspace.openURIInPane selectedEntry.getPath(), pane moveSelectedEntry: -> @@ -445,8 +478,9 @@ class TreeView extends View oldPath = @getActivePath() if oldPath - MoveDialog ?= require './move-dialog' - dialog = new MoveDialog(oldPath) + dialog = new MoveDialog oldPath, + onMove: ({initialPath, newPath}) => + @emitter.emit 'entry-moved', {initialPath, newPath} dialog.attach() # Get the outline of a system call to the current platform's file manager. @@ -483,13 +517,7 @@ class TreeView extends View label: 'File Manager' args: [pathToOpen] - showSelectedEntryInFileManager: -> - entry = @selectedEntry() - return unless entry - - isFile = entry instanceof FileView - {command, args, label} = @fileManagerCommandForPath(entry.getPath(), isFile) - + openInFileManager: (command, args, label, isFile) -> handleError = (errorMessage) -> atom.notifications.addError "Opening #{if isFile then 'file' else 'folder'} in #{label} failed", detail: errorMessage @@ -511,6 +539,20 @@ class TreeView extends View showProcess.onWillThrowError ({error, handle}) -> handle() handleError(error?.message) + showProcess + + showSelectedEntryInFileManager: -> + return unless entry = @selectedEntry() + + isFile = entry.classList.contains('file') + {command, args, label} = @fileManagerCommandForPath(entry.getPath(), isFile) + @openInFileManager(command, args, label, isFile) + + showCurrentFileInFileManager: -> + return unless editor = atom.workspace.getCenter().getActiveTextEditor() + return unless editor.getPath() + {command, args, label} = @fileManagerCommandForPath(editor.getPath(), true) + @openInFileManager(command, args, label, true) openSelectedEntryInNewWindow: -> if pathToOpen = @selectedEntry()?.getPath() @@ -525,17 +567,20 @@ class TreeView extends View oldPath = @getActivePath() return unless oldPath - CopyDialog ?= require './copy-dialog' - dialog = new CopyDialog(oldPath) + dialog = new CopyDialog oldPath, + onCopy: ({initialPath, newPath}) => + @emitter.emit 'entry-copied', {initialPath, newPath} dialog.attach() removeSelectedEntries: -> if @hasFocus() selectedPaths = @selectedPaths() + selectedEntries = @getSelectedEntries() else if activePath = @getActivePath() selectedPaths = [activePath] + selectedEntries = [@entryForPath(activePath)] - return unless selectedPaths and selectedPaths.length > 0 + return unless selectedPaths?.length > 0 for root in @roots if root.getPath() in selectedPaths @@ -548,19 +593,45 @@ class TreeView extends View message: "Are you sure you want to delete the selected #{if selectedPaths.length > 1 then 'items' else 'item'}?" detailedMessage: "You are deleting:\n#{selectedPaths.join('\n')}" buttons: - "Move to Trash": -> + "Move to Trash": => failedDeletions = [] for selectedPath in selectedPaths - if not shell.moveItemToTrash(selectedPath) + # Don't delete entries which no longer exist. This can happen, for example, when: + # * The entry is deleted outside of Atom before "Move to Trash" is selected + # * A folder and one of its children are both selected for deletion, + # but the parent folder is deleted first + continue unless fs.existsSync(selectedPath) + + if shell.moveItemToTrash(selectedPath) + @emitter.emit 'entry-deleted', {path: selectedPath} + else failedDeletions.push "#{selectedPath}" + if repo = repoForPath(selectedPath) repo.getPathStatus(selectedPath) + if failedDeletions.length > 0 - atom.notifications.addError "The following #{if failedDeletions.length > 1 then 'files' else 'file'} couldn't be moved to trash#{if process.platform is 'linux' then " (is `gvfs-trash` installed?)" else ""}", + atom.notifications.addError @formatTrashFailureMessage(failedDeletions), + description: @formatTrashEnabledMessage() detail: "#{failedDeletions.join('\n')}" dismissable: true + + # Focus the first parent folder + @selectEntry(selectedEntries[0].closest('.directory:not(.selected)')) + @updateRoots() if atom.config.get('tree-view.squashDirectoryNames') "Cancel": null + formatTrashFailureMessage: (failedDeletions) -> + fileText = if failedDeletions.length > 1 then 'files' else 'file' + + "The following #{fileText} couldn't be moved to the trash." + + formatTrashEnabledMessage: -> + switch process.platform + when 'linux' then 'Is `gvfs-trash` installed?' + when 'darwin' then 'Is Trash enabled on the volume where the files are stored?' + when 'win32' then 'Is there a Recycle Bin on the drive where the files are stored?' + # Public: Copy the path of the selected entry element. # Save the path in localStorage, so that copying from 2 different # instances of atom works as intended @@ -571,8 +642,8 @@ class TreeView extends View selectedPaths = @selectedPaths() return unless selectedPaths and selectedPaths.length > 0 # save to localStorage so we can paste across multiple open apps - LocalStorage.removeItem('tree-view:cutPath') - LocalStorage['tree-view:copyPath'] = JSON.stringify(selectedPaths) + window.localStorage.removeItem('tree-view:cutPath') + window.localStorage['tree-view:copyPath'] = JSON.stringify(selectedPaths) # Public: Copy the path of the selected entry element. # Save the path in localStorage, so that cutting from 2 different @@ -584,8 +655,8 @@ class TreeView extends View selectedPaths = @selectedPaths() return unless selectedPaths and selectedPaths.length > 0 # save to localStorage so we can paste across multiple open apps - LocalStorage.removeItem('tree-view:copyPath') - LocalStorage['tree-view:cutPath'] = JSON.stringify(selectedPaths) + window.localStorage.removeItem('tree-view:copyPath') + window.localStorage['tree-view:cutPath'] = JSON.stringify(selectedPaths) # Public: Paste a copied or cut item. # If a file is selected, the file's parent directory is used as the @@ -595,8 +666,8 @@ class TreeView extends View # Returns `destination newPath`. pasteEntries: -> selectedEntry = @selectedEntry() - cutPaths = if LocalStorage['tree-view:cutPath'] then JSON.parse(LocalStorage['tree-view:cutPath']) else null - copiedPaths = if LocalStorage['tree-view:copyPath'] then JSON.parse(LocalStorage['tree-view:copyPath']) else null + cutPaths = if window.localStorage['tree-view:cutPath'] then JSON.parse(window.localStorage['tree-view:cutPath']) else null + copiedPaths = if window.localStorage['tree-view:copyPath'] then JSON.parse(window.localStorage['tree-view:copyPath']) else null initialPaths = copiedPaths or cutPaths catchAndShowFileErrors = (operation) -> @@ -609,7 +680,7 @@ class TreeView extends View initialPathIsDirectory = fs.isDirectorySync(initialPath) if selectedEntry and initialPath and fs.existsSync(initialPath) basePath = selectedEntry.getPath() - basePath = path.dirname(basePath) if selectedEntry instanceof FileView + basePath = path.dirname(basePath) if selectedEntry.classList.contains('file') newPath = path.join(basePath, path.basename(initialPath)) if copiedPaths @@ -627,46 +698,56 @@ class TreeView extends View if fs.isDirectorySync(initialPath) # use fs.copy to copy directories since read/write will fail for directories - catchAndShowFileErrors -> fs.copySync(initialPath, newPath) + catchAndShowFileErrors => + fs.copySync(initialPath, newPath) + @emitter.emit 'entry-copied', {initialPath, newPath} else # read the old file and write a new one at target location - catchAndShowFileErrors -> fs.writeFileSync(newPath, fs.readFileSync(initialPath)) + catchAndShowFileErrors => + fs.writeFileSync(newPath, fs.readFileSync(initialPath)) + @emitter.emit 'entry-copied', {initialPath, newPath} else if cutPaths - # Only move the target if the cut target doesn't exists and if the newPath + # Only move the target if the cut target doesn't exist and if the newPath # is not within the initial path unless fs.existsSync(newPath) or newPath.startsWith(initialPath) - catchAndShowFileErrors -> fs.moveSync(initialPath, newPath) + catchAndShowFileErrors => + fs.moveSync(initialPath, newPath) + @emitter.emit 'entry-moved', {initialPath, newPath} add: (isCreatingFile) -> selectedEntry = @selectedEntry() ? @roots[0] selectedPath = selectedEntry?.getPath() ? '' - AddDialog ?= require './add-dialog' dialog = new AddDialog(selectedPath, isCreatingFile) - dialog.on 'directory-created', (event, createdPath) => + dialog.onDidCreateDirectory (createdPath) => @entryForPath(createdPath)?.reload() @selectEntryForPath(createdPath) - false - dialog.on 'file-created', (event, createdPath) -> + @updateRoots() if atom.config.get('tree-view.squashDirectoryNames') + @emitter.emit 'directory-created', {path: createdPath} + dialog.onDidCreateFile (createdPath) => + @entryForPath(createdPath)?.reload() atom.workspace.open(createdPath) - false + @updateRoots() if atom.config.get('tree-view.squashDirectoryNames') + @emitter.emit 'file-created', {path: createdPath} dialog.attach() removeProjectFolder: (e) -> - pathToRemove = $(e.target).closest(".project-root > .header").find(".name").data("path") - - # TODO: remove this conditional once the addition of Project::removePath - # is released. - if atom.project.removePath? - atom.project.removePath(pathToRemove) if pathToRemove? + # Remove the targeted project folder (generally this only happens through the context menu) + pathToRemove = e.target.closest(".project-root > .header")?.querySelector(".name")?.dataset.path + # If an entry is selected, remove that entry's project folder + pathToRemove ?= @selectedEntry()?.closest(".project-root")?.querySelector(".header")?.querySelector(".name")?.dataset.path + # Finally, if only one project folder exists and nothing is selected, remove that folder + pathToRemove ?= @roots[0].querySelector(".header")?.querySelector(".name")?.dataset.path if @roots.length is 1 + atom.project.removePath(pathToRemove) if pathToRemove? selectedEntry: -> - @list[0].querySelector('.selected') + @list.querySelector('.selected') selectEntry: (entry) -> return unless entry? @selectedPath = entry.getPath() + @lastFocusedElement = entry selectedEntries = @getSelectedEntries() if selectedEntries.length > 1 or selectedEntries[0] isnt entry @@ -675,7 +756,7 @@ class TreeView extends View entry getSelectedEntries: -> - @list[0].querySelectorAll('.selected') + @list.querySelectorAll('.selected') deselect: (elementsToDeselect=@getSelectedEntries()) -> selected.classList.remove('selected') for selected in elementsToDeselect @@ -683,42 +764,46 @@ class TreeView extends View scrollTop: (top) -> if top? - @scroller.scrollTop(top) + @element.scrollTop = top else - @scroller.scrollTop() + @element.scrollTop scrollBottom: (bottom) -> if bottom? - @scroller.scrollBottom(bottom) + @element.scrollTop = bottom - @element.offsetHeight else - @scroller.scrollBottom() + @element.scrollTop + @element.offsetHeight - scrollToEntry: (entry) -> - element = if entry instanceof DirectoryView then entry.header else entry - element?.scrollIntoViewIfNeeded(true) # true = center around item if possible + scrollToEntry: (entry, center=true) -> + element = if entry?.classList.contains('directory') then entry.header else entry + element?.scrollIntoViewIfNeeded(center) scrollToBottom: -> - if lastEntry = _.last(@list[0].querySelectorAll('.entry')) + if lastEntry = _.last(@list.querySelectorAll('.entry')) @selectEntry(lastEntry) @scrollToEntry(lastEntry) scrollToTop: -> @selectEntry(@roots[0]) if @roots[0]? - @scrollTop(0) + @element.scrollTop = 0 + + pageUp: -> + @element.scrollTop -= @element.offsetHeight - toggleSide: -> - toggleConfig('tree-view.showOnRightSide') + pageDown: -> + @element.scrollTop += @element.offsetHeight moveEntry: (initialPath, newDirectoryPath) -> if initialPath is newDirectoryPath return entryName = path.basename(initialPath) - newPath = "#{newDirectoryPath}/#{entryName}".replace(/\s+$/, '') + newPath = "#{newDirectoryPath}#{path.sep}#{entryName}".replace(/\s+$/, '') try fs.makeTreeSync(newDirectoryPath) unless fs.existsSync(newDirectoryPath) fs.moveSync(initialPath, newPath) + @emitter.emit 'entry-moved', {initialPath, newPath} if repo = repoForPath(newPath) repo.getPathStatus(initialPath) @@ -728,43 +813,79 @@ class TreeView extends View atom.notifications.addWarning("Failed to move entry #{initialPath} to #{newDirectoryPath}", detail: error.message) onStylesheetsChanged: => + # If visible, force a redraw so the scrollbars are styled correctly based on + # the theme return unless @isVisible() - - # Force a redraw so the scrollbars are styled correctly based on the theme @element.style.display = 'none' @element.offsetWidth @element.style.display = '' onMouseDown: (e) -> + return unless entryToSelect = e.target.closest('.entry') + e.stopPropagation() - # return early if we're opening a contextual menu (right click) during multi-select mode - if @multiSelectEnabled() and - e.currentTarget.classList.contains('selected') and - # mouse right click or ctrl click as right click on darwin platforms - (e.button is 2 or e.ctrlKey and process.platform is 'darwin') - return + # TODO: meta+click and ctrl+click should not do the same thing on Windows. + # Right now removing metaKey if platform is not darwin breaks tests + # that set the metaKey to true when simulating a cmd+click on macos + # and ctrl+click on windows and linux. + cmdKey = e.metaKey or (e.ctrlKey and process.platform isnt 'darwin') + + # return early if clicking on a selected entry + if entryToSelect.classList.contains('selected') - entryToSelect = e.currentTarget + # mouse right click or ctrl click as right click on darwin platforms + if e.button is 2 or (e.ctrlKey and process.platform is 'darwin') + return + else + # allow click on mouseup if not dragging + {shiftKey} = e + @selectOnMouseUp = {shiftKey, cmdKey} + return - if e.shiftKey + if e.shiftKey and cmdKey + # select continuous from @lastFocusedElement but leave others + @selectContinuousEntries(entryToSelect, false) + @toggleMultiSelectMenu() + else if e.shiftKey + # select continuous from @lastFocusedElement and deselect rest @selectContinuousEntries(entryToSelect) - @showMultiSelectMenu() + @toggleMultiSelectMenu() # only allow ctrl click for multi selection on non darwin systems - else if e.metaKey or (e.ctrlKey and process.platform isnt 'darwin') + else if cmdKey @selectMultipleEntries(entryToSelect) - - # only show the multi select menu if more then one file/directory is selected - @showMultiSelectMenu() if @selectedPaths().length > 1 + @lastFocusedElement = entryToSelect + @toggleMultiSelectMenu() else @selectEntry(entryToSelect) @showFullMenu() - onSideToggled: (newValue) -> - @element.dataset.showOnRightSide = newValue - if @isVisible() - @detach() - @attach() + onMouseUp: (e) -> + return unless @selectOnMouseUp? + + {shiftKey, cmdKey} = @selectOnMouseUp + @selectOnMouseUp = null + + return unless entryToSelect = e.target.closest('.entry') + + e.stopPropagation() + + if shiftKey and cmdKey + # select continuous from @lastFocusedElement but leave others + @selectContinuousEntries(entryToSelect, false) + @toggleMultiSelectMenu() + else if shiftKey + # select continuous from @lastFocusedElement and deselect rest + @selectContinuousEntries(entryToSelect) + @toggleMultiSelectMenu() + # only allow ctrl click for multi selection on non darwin systems + else if cmdKey + @deselect([entryToSelect]) + @lastFocusedElement = entryToSelect + @toggleMultiSelectMenu() + else + @selectEntry(entryToSelect) + @showFullMenu() # Public: Return an array of paths from all selected items # @@ -778,16 +899,17 @@ class TreeView extends View # a new given entry. This is shift+click functionality # # Returns array of selected elements - selectContinuousEntries: (entry) -> - currentSelectedEntry = @selectedEntry() - parentContainer = $(entry).parent() - if $.contains(parentContainer[0], currentSelectedEntry) - entries = parentContainer.find('.entry').toArray() + selectContinuousEntries: (entry, deselectOthers = true) -> + currentSelectedEntry = @lastFocusedElement ? @selectedEntry() + parentContainer = entry.parentElement + elements = [] + if parentContainer.contains(currentSelectedEntry) + entries = Array.from(parentContainer.querySelectorAll('.entry')) entryIndex = entries.indexOf(entry) selectedIndex = entries.indexOf(currentSelectedEntry) elements = (entries[i] for i in [entryIndex..selectedIndex]) - @deselect() + @deselect() if deselectOthers element.classList.add('selected') for element in elements elements @@ -803,83 +925,132 @@ class TreeView extends View # Public: Toggle full-menu class on the main list element to display the full context # menu. showFullMenu: -> - @list[0].classList.remove('multi-select') - @list[0].classList.add('full-menu') + @list.classList.remove('multi-select') + @list.classList.add('full-menu') - # Public: Toggle multi-select class on the main list element to display the the + # Public: Toggle multi-select class on the main list element to display the # menu with only items that make sense for multi select functionality showMultiSelectMenu: -> - @list[0].classList.remove('full-menu') - @list[0].classList.add('multi-select') + @list.classList.remove('full-menu') + @list.classList.add('multi-select') + + toggleMultiSelectMenu: -> + if @getSelectedEntries().length > 1 + @showMultiSelectMenu() + else + @showFullMenu() # Public: Check for multi-select class on the main list # # Returns boolean multiSelectEnabled: -> - @list[0].classList.contains('multi-select') + @list.classList.contains('multi-select') onDragEnter: (e) => - e.stopPropagation() - entry = e.currentTarget.parentNode - @dragEventCounts.set(entry, 0) unless @dragEventCounts.get(entry) - entry.classList.add('selected') if @dragEventCounts.get(entry) is 0 - @dragEventCounts.set(entry, @dragEventCounts.get(entry) + 1) + if header = e.target.closest('.entry.directory > .header') + return if @rootDragAndDrop.isDragging(e) - onDragLeave: (e) => - e.stopPropagation() - entry = e.currentTarget.parentNode - @dragEventCounts.set(entry, @dragEventCounts.get(entry) - 1) - entry.classList.remove('selected') if @dragEventCounts.get(entry) is 0 + e.stopPropagation() - # Handle entry name object dragstart event - onDragStart: (e) -> - e.stopPropagation() + entry = header.parentNode + @dragEventCounts.set(entry, 0) unless @dragEventCounts.get(entry) + entry.classList.add('selected') if @dragEventCounts.get(entry) is 0 + @dragEventCounts.set(entry, @dragEventCounts.get(entry) + 1) - target = $(e.currentTarget).find(".name") - initialPath = target.data("path") - - style = getStyleObject(target[0]) + onDragLeave: (e) => + if header = e.target.closest('.entry.directory > .header') + return if @rootDragAndDrop.isDragging(e) - fileNameElement = target.clone() - .css(style) - .css( - position: 'absolute' - top: 0 - left: 0 - ) - fileNameElement.appendTo(document.body) + e.stopPropagation() - e.originalEvent.dataTransfer.effectAllowed = "move" - e.originalEvent.dataTransfer.setDragImage(fileNameElement[0], 0, 0) - e.originalEvent.dataTransfer.setData("initialPath", initialPath) + entry = header.parentNode + @dragEventCounts.set(entry, @dragEventCounts.get(entry) - 1) + entry.classList.remove('selected') if @dragEventCounts.get(entry) is 0 - window.requestAnimationFrame -> - fileNameElement.remove() + # Handle entry name object dragstart event + onDragStart: (e) -> + @selectOnMouseUp = null + if entry = e.target.closest('.entry') + e.stopPropagation() + + if @rootDragAndDrop.canDragStart(e) + return @rootDragAndDrop.onDragStart(e) + + initialPaths = [] + dragImage = document.createElement("ol") + dragImage.classList.add("entries", "list-tree") + dragImage.style.position = "absolute" + dragImage.style.top = 0 + dragImage.style.left = 0 + # Ensure the cloned file name element is rendered on a separate GPU layer + # to prevent overlapping elements located at (0px, 0px) from being used as + # the drag image. + dragImage.style.willChange = "transform" + + for target in @getSelectedEntries() + entryPath = target.querySelector(".name").dataset.path + unless path.dirname(entryPath) in initialPaths + initialPaths.push(entryPath) + newElement = target.cloneNode(true) + if newElement.classList.contains("directory") + newElement.querySelector(".entries").remove() + for key, value of getStyleObject(target) + newElement.style[key] = value + newElement.style.paddingLeft = "1em" + newElement.style.paddingRight = "1em" + dragImage.append(newElement) + + + document.body.appendChild(dragImage) + + e.dataTransfer.effectAllowed = "move" + e.dataTransfer.setDragImage(dragImage, 0, 0) + e.dataTransfer.setData("initialPaths", initialPaths) + + window.requestAnimationFrame -> + dragImage.remove() # Handle entry dragover event; reset default dragover actions onDragOver: (e) -> - e.preventDefault() - e.stopPropagation() + if entry = e.target.closest('.entry') + return if @rootDragAndDrop.isDragging(e) + + e.preventDefault() + e.stopPropagation() + + if @dragEventCounts.get(entry) > 0 and not entry.classList.contains('selected') + entry.classList.add('selected') # Handle entry drop event onDrop: (e) -> - e.preventDefault() - e.stopPropagation() + if entry = e.target.closest('.entry') + return if @rootDragAndDrop.isDragging(e) - entry = e.currentTarget - return unless entry instanceof DirectoryView + e.preventDefault() + e.stopPropagation() - entry.classList.remove('selected') + return unless entry.classList.contains('directory') - newDirectoryPath = $(entry).find(".name").data("path") - return false unless newDirectoryPath + newDirectoryPath = entry.querySelector('.name')?.dataset.path + return false unless newDirectoryPath - initialPath = e.originalEvent.dataTransfer.getData("initialPath") + initialPaths = e.dataTransfer.getData('initialPaths') - if initialPath - # Drop event from Atom - @moveEntry(initialPath, newDirectoryPath) - else - # Drop event from OS - for file in e.originalEvent.dataTransfer.files - @moveEntry(file.path, newDirectoryPath) + if initialPaths + # Drop event from Atom + initialPaths = initialPaths.split(',') + return if initialPaths.includes(newDirectoryPath) + + entry.classList.remove('selected') + + # iterate backwards so files in a dir are moved before the dir itself + for initialPath in initialPaths by -1 + @moveEntry(initialPath, newDirectoryPath) + else + # Drop event from OS + entry.classList.remove('selected') + for file in e.dataTransfer.files + @moveEntry(file.path, newDirectoryPath) + + isVisible: -> + @element.offsetWidth isnt 0 or @element.offsetHeight isnt 0 diff --git a/menus/tree-view.cson b/menus/tree-view.cson index 50a562b1..da5fffc5 100644 --- a/menus/tree-view.cson +++ b/menus/tree-view.cson @@ -20,7 +20,7 @@ ] 'context-menu': - '.tree-view.full-menu': [ + '.tree-view .full-menu': [ {'label': 'New File', 'command': 'tree-view:add-file'} {'label': 'New Folder', 'command': 'tree-view:add-folder'} {'type': 'separator'} @@ -41,7 +41,7 @@ {'label': 'Open In New Window', 'command': 'tree-view:open-in-new-window'} ] - '.tree-view.full-menu [is="tree-view-file"]': [ + '.tree-view .full-menu [is="tree-view-file"]': [ {'label': 'Split Up', 'command': 'tree-view:open-selected-entry-up'} {'label': 'Split Down', 'command': 'tree-view:open-selected-entry-down'} {'label': 'Split Left', 'command': 'tree-view:open-selected-entry-left'} @@ -49,7 +49,7 @@ {'type': 'separator'} ] - '.tree-view.full-menu .project-root > .header': [ + '.tree-view .full-menu .project-root > .header': [ {'label': 'New File', 'command': 'tree-view:add-file'} {'label': 'New Folder', 'command': 'tree-view:add-folder'} {'type': 'separator'} @@ -71,19 +71,19 @@ {'label': 'Open In New Window', 'command': 'tree-view:open-in-new-window'} ] - '.platform-darwin .tree-view.full-menu': [ + '.platform-darwin .tree-view .full-menu': [ {'label': 'Show in Finder', 'command': 'tree-view:show-in-file-manager'} ] - '.platform-win32 .tree-view.full-menu': [ + '.platform-win32 .tree-view .full-menu': [ {'label': 'Show in Explorer', 'command': 'tree-view:show-in-file-manager'} ] - '.platform-linux .tree-view.full-menu': [ + '.platform-linux .tree-view .full-menu': [ {'label': 'Show in File Manager', 'command': 'tree-view:show-in-file-manager'} ] - '.tree-view.multi-select': [ + '.tree-view .multi-select': [ {'label': 'Delete', 'command': 'tree-view:remove'} {'label': 'Copy', 'command': 'tree-view:copy'} {'label': 'Cut', 'command': 'tree-view:cut'} @@ -98,3 +98,27 @@ {'label': 'Rename', 'command': 'tree-view:rename'} {'label': 'Reveal in Tree View', 'command': 'tree-view:reveal-active-file'} ] + + '.platform-darwin atom-pane[data-active-item-path] .tab.active': [ + {'label': 'Show In Finder', 'command': 'tree-view:show-current-file-in-file-manager'} + ] + + '.platform-win32 atom-pane[data-active-item-path] .tab.active': [ + {'label': 'Show In Explorer', 'command': 'tree-view:show-current-file-in-file-manager'} + ] + + '.platform-linux atom-pane[data-active-item-path] .tab.active': [ + {'label': 'Show In File Manager', 'command': 'tree-view:show-current-file-in-file-manager'} + ] + + '.platform-darwin atom-text-editor:not([mini])': [ + {'label': 'Show In Finder', 'command': 'tree-view:show-current-file-in-file-manager'} + ] + + '.platform-win32 atom-text-editor:not([mini])': [ + {'label': 'Show In Explorer', 'command': 'tree-view:show-current-file-in-file-manager'} + ] + + '.platform-linux atom-text-editor:not([mini])': [ + {'label': 'Show In File Manager', 'command': 'tree-view:show-current-file-in-file-manager'} + ] diff --git a/package.json b/package.json index 86d7cb8d..c2f05f06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tree-view", - "version": "0.208.0", + "version": "0.218.0", "main": "./lib/main", "description": "Explore and open files in the current project.", "repository": "https://github.com/atom/tree-view", @@ -10,17 +10,19 @@ }, "private": true, "dependencies": { - "atom-space-pen-views": "^2.1.1", "event-kit": "^1.0.0", - "fs-plus": "^2.3.0", + "fs-plus": "^3.0.0", "minimatch": "~0.3.0", - "pathwatcher": "^6.2", + "pathwatcher": "^8.0.0", "temp": "~0.8.1", "underscore-plus": "^1.0.0" }, "devDependencies": { "coffeelint": "^1.9.7" }, + "deserializers": { + "TreeView": "getTreeViewInstance" + }, "consumedServices": { "atom.file-icons": { "versions": { @@ -46,11 +48,6 @@ "default": false, "description": "Don't show items matched by the `Ignored Names` core config setting." }, - "showOnRightSide": { - "type": "boolean", - "default": false, - "description": "Show the tree view on the right side of the editor instead of the left." - }, "sortFoldersBeforeFiles": { "type": "boolean", "default": true, @@ -68,7 +65,8 @@ }, "alwaysOpenExisting": { "type": "boolean", - "default": false + "default": false, + "description": "When opening a file, always focus an already-existing view of the file even if it's in a another pane." } } } diff --git a/spec/default-file-icons-spec.coffee b/spec/default-file-icons-spec.coffee index ed23e656..83ea9fa2 100644 --- a/spec/default-file-icons-spec.coffee +++ b/spec/default-file-icons-spec.coffee @@ -1,6 +1,5 @@ fs = require 'fs-plus' path = require 'path' -process = require 'process' temp = require('temp').track() DefaultFileIcons = require '../lib/default-file-icons' @@ -42,12 +41,7 @@ describe 'DefaultFileIcons', -> filePath = path.join(tempDir, 'foo.bar') linkPath = path.join(tempDir, 'link.bar') fs.writeFileSync(filePath, '') - try - fs.symlinkSync(filePath, linkPath) - catch err - # Symlinks are Administrator only on Windows - return if err.code is 'EPERM' and process.platform is 'win32' - throw err + fs.symlinkSync(filePath, linkPath, 'junction') expect(fileIcons.iconClassForPath(linkPath)).toEqual('icon-file-symlink-file') @@ -55,11 +49,6 @@ describe 'DefaultFileIcons', -> filePath = path.join(tempDir, 'foo.zip') linkPath = path.join(tempDir, 'link.zip') fs.writeFileSync(filePath, '') - try - fs.symlinkSync(filePath, linkPath) - catch err - # Symlinks are Administrator only on Windows - return if err.code is 'EPERM' and process.platform is 'win32' - throw err + fs.symlinkSync(filePath, linkPath, 'junction') expect(fileIcons.iconClassForPath(linkPath)).toEqual('icon-file-symlink-file') diff --git a/spec/event-helpers.coffee b/spec/event-helpers.coffee index 9ebb803e..0ad87f22 100644 --- a/spec/event-helpers.coffee +++ b/spec/event-helpers.coffee @@ -1,5 +1,3 @@ -{$} = require 'atom-space-pen-views' - module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget) -> dataTransfer = data: {} @@ -7,20 +5,30 @@ module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget) -> getData: (key) -> @data[key] setDragImage: (@image) -> return - dragStartEvent = $.Event() - dragStartEvent.target = dragged - dragStartEvent.currentTarget = dragged - dragStartEvent.originalEvent = {dataTransfer} + Object.defineProperty( + dataTransfer, + 'items', + get: -> + Object.keys(dataTransfer.data).map((key) -> {type: key}) + ) + + for entry in dragged + entry.classList.add('selected') + + dragStartEvent = new DragEvent('dragstart') + Object.defineProperty(dragStartEvent, 'target', value: dragged[0]) + Object.defineProperty(dragStartEvent, 'currentTarget', value: dragged[0]) + Object.defineProperty(dragStartEvent, 'dataTransfer', value: dataTransfer) - dropEvent = $.Event() - dropEvent.target = dropTarget - dropEvent.currentTarget = dropTarget - dropEvent.originalEvent = {dataTransfer} + dropEvent = new DragEvent('drop') + Object.defineProperty(dropEvent, 'target', value: dropTarget) + Object.defineProperty(dropEvent, 'currentTarget', value: dropTarget) + Object.defineProperty(dropEvent, 'dataTransfer', value: dataTransfer) - dragEnterEvent = $.Event() - dragEnterEvent.target = enterTarget - dragEnterEvent.currentTarget = enterTarget - dragEnterEvent.originalEvent = {dataTransfer} + dragEnterEvent = new DragEvent('dragenter') + Object.defineProperty(dragEnterEvent, 'target', value: enterTarget) + Object.defineProperty(dragEnterEvent, 'currentTarget', value: enterTarget) + Object.defineProperty(dragEnterEvent, 'dataTransfer', value: dataTransfer) [dragStartEvent, dragEnterEvent, dropEvent] @@ -31,12 +39,72 @@ module.exports.buildExternalDropEvent = (filePaths, dropTarget) -> getData: (key) -> @data[key] files: [] - dropEvent = $.Event() - dropEvent.target = dropTarget - dropEvent.currentTarget = dropTarget - dropEvent.originalEvent = {dataTransfer} + Object.defineProperty( + dataTransfer, + 'items', + get: -> + Object.keys(dataTransfer.data).map((key) -> {type: key}) + ) + + dropEvent = new DragEvent('drop') + Object.defineProperty(dropEvent, 'target', value: dropTarget) + Object.defineProperty(dropEvent, 'currentTarget', value: dropTarget) + Object.defineProperty(dropEvent, 'dataTransfer', value: dataTransfer) for filePath in filePaths - dropEvent.originalEvent.dataTransfer.files.push({path: filePath}) + dropEvent.dataTransfer.files.push({path: filePath}) + + dropEvent + +buildElementPositionalDragEvents = (el, dataTransfer, currentTargetSelector) -> + if not el? + return {} + + currentTarget = if currentTargetSelector then el.closest(currentTargetSelector) else el + + topEvent = new DragEvent('dragstart') + Object.defineProperty(topEvent, 'target', value: el) + Object.defineProperty(topEvent, 'currentTarget', value: currentTarget) + Object.defineProperty(topEvent, 'dataTransfer', value: dataTransfer) + Object.defineProperty(topEvent, 'pageY', value: el.getBoundingClientRect().top) + + middleEvent = new DragEvent('dragover') + Object.defineProperty(middleEvent, 'target', value: el) + Object.defineProperty(middleEvent, 'currentTarget', value: currentTarget) + Object.defineProperty(middleEvent, 'dataTransfer', value: dataTransfer) + Object.defineProperty(middleEvent, 'pageY', value: el.getBoundingClientRect().top + el.offsetHeight * 0.5) + + bottomEvent = new DragEvent('dragend') + Object.defineProperty(bottomEvent, 'target', value: el) + Object.defineProperty(bottomEvent, 'currentTarget', value: currentTarget) + Object.defineProperty(bottomEvent, 'dataTransfer', value: dataTransfer) + Object.defineProperty(bottomEvent, 'pageY', value: el.getBoundingClientRect().bottom) + + {top: topEvent, middle: middleEvent, bottom: bottomEvent} + + +module.exports.buildPositionalDragEvents = (dragged, target, currentTargetSelector) -> + dataTransfer = + data: {} + setData: (key, value) -> @data[key] = "#{value}" # Drag events stringify data values + getData: (key) -> @data[key] + setDragImage: (@image) -> return + + Object.defineProperty( + dataTransfer, + 'items', + get: -> + Object.keys(dataTransfer.data).map((key) -> {type: key}) + ) + + dragStartEvent = new DragEvent('dragstart') + Object.defineProperty(dragStartEvent, 'target', value: dragged) + Object.defineProperty(dragStartEvent, 'currentTarget', value: dragged) + Object.defineProperty(dragStartEvent, 'dataTransfer', value: dataTransfer) + + dragEndEvent = new DragEvent('dragend') + Object.defineProperty(dragEndEvent, 'target', value: dragged) + Object.defineProperty(dragEndEvent, 'currentTarget', value: dragged) + Object.defineProperty(dragEndEvent, 'dataTransfer', value: dataTransfer) - dropEvent \ No newline at end of file + [dragStartEvent, buildElementPositionalDragEvents(target, dataTransfer, currentTargetSelector), dragEndEvent] diff --git a/spec/file-stats-spec.coffee b/spec/file-stats-spec.coffee new file mode 100644 index 00000000..1bcf0e04 --- /dev/null +++ b/spec/file-stats-spec.coffee @@ -0,0 +1,75 @@ +_ = require 'underscore-plus' +fs = require 'fs-plus' +path = require 'path' +temp = require('temp').track() + +describe "FileStats", -> + describe "provision of filesystem stats", -> + [file1Data, file2Data, timeStarted, treeView] = ["ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789"] + + beforeEach -> + jasmine.useRealClock() + timeStarted = Date.now() + rootDirPath = fs.absolute(temp.mkdirSync("tree-view")) + subdirPath = path.join(rootDirPath, "subdir") + filePath1 = path.join(rootDirPath, "file1.txt") + filePath2 = path.join(subdirPath, "file2.txt") + + fs.makeTreeSync(subdirPath) + fs.writeFileSync(filePath1, file1Data) + fs.writeFileSync(filePath2, file2Data) + atom.project.setPaths([rootDirPath]) + + waitsFor (done) -> + atom.workspace.onDidOpen(done) + atom.packages.activatePackage("tree-view") + + runs -> + treeView = atom.workspace.getLeftDock().getActivePaneItem() + + afterEach -> + temp.cleanup() + + it "passes stats to File instances", -> + stats = treeView.roots[0].directory.entries.get("file1.txt").stats + expect(stats).toBeDefined() + expect(stats.mtime).toBeDefined() + expect(stats.size).toEqual(file1Data.length) + + it "passes stats to Directory instances", -> + stats = treeView.roots[0].directory.entries.get("subdir").stats + expect(stats).toBeDefined() + expect(stats.mtime).toBeDefined() + + it "passes stats to a root directory when initialised", -> + expect(treeView.roots[0].directory.stats).toBeDefined() + + it "passes stats to File instances in subdirectories", -> + treeView.element.querySelector(".entries > li").expand() + subdir = treeView.roots[0].directory.entries.get("subdir") + stats = subdir.entries.get("file2.txt").stats + expect(stats).toBeDefined() + expect(stats.size).toEqual(file2Data.length) + + it "converts date-stats to timestamps", -> + stats = treeView.roots[0].directory.entries.get("file1.txt").stats + stamp = stats.mtime + expect(_.isDate stamp).toBe(false) + expect(typeof stamp).toBe("number") + expect(Number.isNaN stamp).toBe(false) + + it "accurately converts timestamps", -> + stats = treeView.roots[0].directory.entries.get("file1.txt").stats + # Two minutes should be enough + expect(Math.abs stats.mtime - timeStarted).toBeLessThan(120000) + + describe "virtual filepaths", -> + beforeEach -> + atom.project.setPaths([]) + waitsForPromise -> Promise.all [ + atom.packages.activatePackage("tree-view") + atom.packages.activatePackage("about") + ] + + it "doesn't throw an exception when accessing virtual filepaths", -> + atom.project.setPaths(["atom://about"]) diff --git a/spec/tree-view-spec.coffee b/spec/tree-view-package-spec.coffee similarity index 50% rename from spec/tree-view-spec.coffee rename to spec/tree-view-package-spec.coffee index f8c25ef3..bd952ea6 100644 --- a/spec/tree-view-spec.coffee +++ b/spec/tree-view-package-spec.coffee @@ -1,23 +1,28 @@ _ = require 'underscore-plus' -{$, $$} = require 'atom-space-pen-views' fs = require 'fs-plus' path = require 'path' temp = require('temp').track() os = require 'os' +{remote, shell} = require 'electron' +Directory = require '../lib/directory' eventHelpers = require "./event-helpers" -waitsForFileToOpen = (causeFileToOpen) -> +DefaultFileIcons = require '../lib/default-file-icons' +FileIcons = require '../lib/file-icons' + +waitForPackageActivation = -> + waitsForPromise -> + atom.packages.activatePackage('tree-view') + waitsForPromise -> + atom.packages.getActivePackage('tree-view').mainModule.treeViewOpenPromise + +waitForWorkspaceOpenEvent = (causeFileToOpen) -> waitsFor (done) -> - disposable = atom.workspace.onDidOpen -> + disposable = atom.workspace.onDidOpen ({item}) -> disposable.dispose() done() causeFileToOpen() -clickEvent = (properties) -> - event = $.Event('click') - _.extend(event, properties) if properties? - event - setupPaneFiles = -> rootDirPath = fs.absolute(temp.mkdirSync('tree-view')) @@ -39,6 +44,7 @@ describe "TreeView", -> treeView.selectEntryForPath atom.project.getDirectories()[0].resolve pathToSelect beforeEach -> + expect(atom.workspace.getLeftDock().getActivePaneItem()).toBeUndefined() expect(atom.config.get('core.allowPendingPaneItems')).toBeTruthy() fixturesPath = atom.project.getPaths()[0] @@ -48,45 +54,47 @@ describe "TreeView", -> workspaceElement = atom.views.getView(atom.workspace) - waitsForPromise -> - atom.packages.activatePackage("tree-view") + waitForPackageActivation() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - - root1 = $(treeView.roots[0]) - root2 = $(treeView.roots[1]) - sampleJs = treeView.find('.file:contains(tree-view.js)') - sampleTxt = treeView.find('.file:contains(tree-view.txt)') - - expect(treeView.roots[0].directory.watchSubscription).toBeTruthy() + moduleInstance = atom.packages.getActivePackage('tree-view').mainModule.getTreeViewInstance() + treeView = atom.workspace.getLeftDock().getActivePaneItem() + files = treeView.element.querySelectorAll('.file') + root1 = treeView.roots[0] + root2 = treeView.roots[1] + sampleJs = files[0] + sampleTxt = files[1] + expect(root1.directory.watchSubscription).toBeTruthy() afterEach -> temp.cleanup() + if treeViewOpenPromise = atom.packages.getActivePackage('tree-view')?.mainModule.treeViewOpenPromise + waitsForPromise -> treeViewOpenPromise - describe ".initialize(project)", -> + describe "on package activation", -> it "renders the root directories of the project and their contents alphabetically with subdirectories first, in a collapsed state", -> - expect(root1.find('> .header .disclosure-arrow')).not.toHaveClass('expanded') - expect(root1.find('> .header .name')).toHaveText('root-dir1') + expect(root1.querySelector('.header .disclosure-arrow')).not.toHaveClass('expanded') + expect(root1.querySelector('.header .name')).toHaveText('root-dir1') - rootEntries = root1.find('.entries') - subdir0 = rootEntries.find('> li:eq(0)') + rootEntries = root1.querySelectorAll('.entries li') + subdir0 = rootEntries[0] expect(subdir0).not.toHaveClass('expanded') - expect(subdir0.find('.name')).toHaveText('dir1') + expect(subdir0.querySelector('.name')).toHaveText('dir1') - subdir2 = rootEntries.find('> li:eq(1)') + subdir2 = rootEntries[1] expect(subdir2).not.toHaveClass('expanded') - expect(subdir2.find('.name')).toHaveText('dir2') + expect(subdir2.querySelector('.name')).toHaveText('dir2') - expect(subdir0.find('[data-name="dir1"]')).toExist() - expect(subdir2.find('[data-name="dir2"]')).toExist() + expect(subdir0.querySelector('[data-name="dir1"]')).toExist() + expect(subdir2.querySelector('[data-name="dir2"]')).toExist() - expect(rootEntries.find('> .file:contains(tree-view.js)')).toExist() - expect(rootEntries.find('> .file:contains(tree-view.txt)')).toExist() + file1 = root1.querySelector('.file [data-name="tree-view.js"]') + expect(file1).toExist() + expect(file1).toHaveText('tree-view.js') - expect(rootEntries.find('> .file [data-name="tree-view.js"]')).toExist() - expect(rootEntries.find('> .file [data-name="tree-view.txt"]')).toExist() + file2 = root1.querySelector('.file [data-name="tree-view.txt"]') + expect(file2).toExist() + expect(file2).toHaveText('tree-view.txt') it "selects the root folder", -> expect(treeView.selectedEntry()).toEqual(treeView.roots[0]) @@ -97,25 +105,22 @@ describe "TreeView", -> describe "when the project has no path", -> beforeEach -> atom.project.setPaths([]) - atom.packages.deactivatePackage("tree-view") - waitsForPromise -> - atom.packages.activatePackage("tree-view") + Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom + runs -> + expect(atom.workspace.getLeftDock().getActivePaneItem()).toBeUndefined() + + waitsForPromise -> atom.packages.activatePackage("tree-view") runs -> - treeView = atom.packages.getActivePackage("tree-view").mainModule.createView() + treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance() it "does not attach to the workspace or create a root node when initialized", -> - expect(treeView.hasParent()).toBeFalsy() + expect(treeView.element.parentElement).toBeFalsy() expect(treeView.roots).toHaveLength(0) it "does not attach to the workspace or create a root node when attach() is called", -> - treeView.attach() - expect(treeView.hasParent()).toBeFalsy() - expect(treeView.roots).toHaveLength(0) - - it "serializes without throwing an exception", -> - expect(-> treeView.serialize()).not.toThrow() + expect(atom.workspace.getLeftDock().getActivePaneItem()).toBeUndefined() it "does not throw an exception when files are opened", -> filePath = path.join(os.tmpdir(), 'non-project-file.txt') @@ -131,47 +136,61 @@ describe "TreeView", -> waitsForPromise -> atom.workspace.open(filePath) + waitsForPromise -> + treeView.revealActiveFile() + runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeFalsy() + expect(treeView.element.parentElement).toBeFalsy() expect(treeView.roots).toHaveLength(0) describe "when the project is assigned a path because a new buffer is saved", -> it "creates a root directory view and attaches to the workspace", -> + projectPath = temp.mkdirSync('atom-project') + waitsForPromise -> atom.workspace.open() + waitsFor (done) -> + atom.workspace.getCenter().getActivePaneItem().saveAs(path.join(projectPath, 'test.txt')) + atom.workspace.onDidOpen(done) + runs -> - projectPath = temp.mkdirSync('atom-project') - atom.workspace.getActivePaneItem().saveAs(path.join(projectPath, 'test.txt')) - expect(treeView.hasParent()).toBeTruthy() + treeView = atom.workspace.getLeftDock().getActivePaneItem() expect(treeView.roots).toHaveLength(1) expect(fs.absolute(treeView.roots[0].getPath())).toBe fs.absolute(projectPath) describe "when the root view is opened to a file path", -> - it "does not attach to the workspace but does create a root node when initialized", -> - atom.packages.deactivatePackage("tree-view") - atom.packages.packageStates = {} + it "does not show the dock on activation", -> waitsForPromise -> - atom.workspace.open('tree-view.js') + Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom + + runs -> + atom.packages.packageStates = {} + atom.workspace.getLeftDock().hide() + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) waitsForPromise -> - atom.packages.activatePackage('tree-view') + atom.workspace.open('tree-view.js') runs -> - treeView = atom.packages.getActivePackage("tree-view").mainModule.createView() - expect(treeView.hasParent()).toBeFalsy() - expect(treeView.roots).toHaveLength(2) + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + + waitForPackageActivation() + + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + atom.project.addPath(path.join(__dirname, 'fixtures')) + + waitsFor -> atom.workspace.getLeftDock().isVisible() describe "when the root view is opened to a directory", -> it "attaches to the workspace", -> - waitsForPromise -> - atom.packages.activatePackage('tree-view') + waitsForPromise -> atom.packages.activatePackage('tree-view') runs -> - treeView = atom.packages.getActivePackage("tree-view").mainModule.createView() - expect(treeView.hasParent()).toBeTruthy() + treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance() + expect(treeView.element.parentElement).toBeTruthy() expect(treeView.roots).toHaveLength(2) describe "when the project is a .git folder", -> @@ -179,8 +198,12 @@ describe "TreeView", -> dotGit = path.join(temp.mkdirSync('repo'), '.git') fs.makeTreeSync(dotGit) atom.project.setPaths([dotGit]) - atom.packages.deactivatePackage("tree-view") - atom.packages.packageStates = {} + + waitsForPromise -> + Promise.resolve(atom.packages.deactivatePackage("tree-view")) # Wrapped for both async and non-async versions of Atom + + runs -> + atom.packages.packageStates = {} waitsForPromise -> atom.packages.activatePackage('tree-view') @@ -189,137 +212,28 @@ describe "TreeView", -> {treeView} = atom.packages.getActivePackage("tree-view").mainModule expect(treeView).toBeFalsy() - describe "serialization", -> - it "restores the attached/detached state of the tree-view", -> - jasmine.attachToDOM(workspaceElement) - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - expect(atom.workspace.getLeftPanels().length).toBe(0) - - atom.packages.deactivatePackage("tree-view") - - waitsForPromise -> - atom.packages.activatePackage("tree-view") - - runs -> - expect(atom.workspace.getLeftPanels().length).toBe(0) - - it "restores expanded directories and selected file when deserialized", -> - root1.find('.directory:contains(dir1)').click() - - waitsForFileToOpen -> - sampleJs.click() - - runs -> - atom.packages.deactivatePackage("tree-view") - - waitsForPromise -> - atom.packages.activatePackage("tree-view") - - runs -> - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - expect(treeView).toExist() - expect($(treeView.selectedEntry())).toMatchSelector(".file:contains(tree-view.js)") - root1 = $(treeView.roots[0]) - expect(root1.find(".directory:contains(dir1)")).toHaveClass("expanded") - - it "restores the focus state of the tree view", -> - jasmine.attachToDOM(workspaceElement) - treeView.focus() - expect(treeView.list).toMatchSelector ':focus' - atom.packages.deactivatePackage("tree-view") - - waitsForPromise -> - atom.packages.activatePackage("tree-view") - - runs -> - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - expect(treeView.list).toMatchSelector ':focus' - - it "restores the scroll top when toggled", -> - workspaceElement.style.height = '5px' - jasmine.attachToDOM(workspaceElement) - expect(treeView).toBeVisible() - treeView.focus() - - treeView.scrollTop(10) - expect(treeView.scrollTop()).toBe(10) - - runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - waitsFor -> treeView.is(':hidden') - - runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - waitsFor -> treeView.is(':visible') - - runs -> expect(treeView.scrollTop()).toBe(10) - - it "restores the scroll left when toggled", -> - treeView.width(5) - jasmine.attachToDOM(workspaceElement) - expect(treeView).toBeVisible() - treeView.focus() - - treeView.scroller.scrollLeft(5) - expect(treeView.scroller.scrollLeft()).toBe(5) - - runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - waitsFor -> treeView.is(':hidden') - - runs -> atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - waitsFor -> treeView.is(':visible') - - runs -> expect(treeView.scroller.scrollLeft()).toBe(5) - describe "when tree-view:toggle is triggered on the root view", -> beforeEach -> jasmine.attachToDOM(workspaceElement) describe "when the tree view is visible", -> beforeEach -> - expect(treeView).toBeVisible() - - describe "when the tree view is focused", -> - it "hides the tree view", -> - treeView.focus() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - expect(treeView).toBeHidden() + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) - describe "when the tree view is not focused", -> - it "hides the tree view", -> - $(workspaceElement).focus() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - expect(treeView).toBeHidden() + it "hides the tree view", -> + workspaceElement.focus() + waitsForPromise -> treeView.toggle() + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) describe "when the tree view is hidden", -> it "shows and focuses the tree view", -> - treeView.detach() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - expect(treeView.hasParent()).toBeTruthy() - expect(treeView.list).toMatchSelector(':focus') - - describe "when tree-view:toggle-side is triggered on the root view", -> - describe "when the tree view is on the left", -> - it "moves the tree view to the right", -> - expect(treeView).toBeVisible() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side') - expect(treeView).toMatchSelector('[data-show-on-right-side="true"]') - - describe "when the tree view is on the right", -> - beforeEach -> - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side') - - it "moves the tree view to the left", -> - expect(treeView).toBeVisible() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side') - expect(treeView).toMatchSelector('[data-show-on-right-side="false"]') - - describe "when the tree view is hidden", -> - it "shows the tree view on the other side next time it is opened", -> - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side') - atom.commands.dispatch(workspaceElement, 'tree-view:toggle') - expect(atom.workspace.getLeftPanels().length).toBe 0 - treeView = $(atom.workspace.getRightPanels()[0].getItem()).view() - expect(treeView).toMatchSelector('[data-show-on-right-side="true"]') + atom.workspace.getLeftDock().hide() + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + waitsForPromise -> treeView.toggle() + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.element).toHaveFocus() describe "when tree-view:toggle-focus is triggered on the root view", -> beforeEach -> @@ -327,10 +241,11 @@ describe "TreeView", -> describe "when the tree view is hidden", -> it "shows and focuses the tree view", -> - treeView.detach() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus') - expect(treeView.hasParent()).toBeTruthy() - expect(treeView.list).toMatchSelector(':focus') + atom.workspace.getLeftDock().hide() + waitsForPromise -> treeView.toggleFocus() + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.element).toHaveFocus() describe "when the tree view is shown", -> it "focuses the tree view", -> @@ -338,11 +253,13 @@ describe "TreeView", -> atom.workspace.open() # When we call focus below, we want an editor to become focused runs -> - $(workspaceElement).focus() - expect(treeView).toBeVisible() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus') - expect(treeView).toBeVisible() - expect(treeView.list).toMatchSelector(':focus') + workspaceElement.focus() + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + waitsForPromise -> treeView.toggleFocus() + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.element).toHaveFocus() describe "when the tree view is focused", -> it "unfocuses the tree view", -> @@ -351,14 +268,29 @@ describe "TreeView", -> runs -> treeView.focus() - expect(treeView).toBeVisible() - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-focus') - expect(treeView).toBeVisible() - expect(treeView.list).not.toMatchSelector(':focus') + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + treeView.toggleFocus() + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.element).not.toHaveFocus() + + describe "when the tree-view is destroyed", -> + it "can correctly re-create the tree-view", -> + treeView = atom.workspace.getLeftDock().getActivePaneItem() + treeViewHTML = treeView.element.outerHTML + treeView.roots[0].collapse() + treeView.destroy() + + waitForWorkspaceOpenEvent -> + atom.commands.dispatch(atom.views.getView(atom.workspace), 'tree-view:toggle') - describe "when tree-view:reveal-active-file is triggered on the root view", -> + runs -> + treeView2 = atom.workspace.getLeftDock().getActivePaneItem() + treeView2.roots[0].expand() + expect(treeView2.element.outerHTML).toBe(treeViewHTML) + + describe "when tree-view:reveal-active-file is triggered", -> beforeEach -> - treeView.detach() + atom.workspace.getLeftDock().hide() spyOn(treeView, 'focus') describe "if the current file has a path", -> @@ -367,20 +299,24 @@ describe "TreeView", -> atom.config.set "tree-view.focusOnReveal", true waitsForPromise -> - atom.workspace.open(path.join(atom.project.getPaths()[0], 'dir1', 'file1')) + atom.workspace.open(path.join(path1, 'dir1', 'file1')) + + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() + expect(treeView.element.parentElement).toBeTruthy() expect(treeView.focus).toHaveBeenCalled() waitsForPromise -> treeView.focus.reset() - atom.workspace.open(path.join(atom.project.getPaths()[1], 'dir3', 'file3')) + atom.workspace.open(path.join(path2, 'dir3', 'file3')) + + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() + expect(treeView.element.parentElement).toBeTruthy() expect(treeView.focus).toHaveBeenCalled() describe "if the tree-view.focusOnReveal config option is false", -> @@ -388,39 +324,71 @@ describe "TreeView", -> atom.config.set "tree-view.focusOnReveal", false waitsForPromise -> - atom.workspace.open(path.join(atom.project.getPaths()[0], 'dir1', 'file1')) + atom.workspace.open(path.join(path1, 'dir1', 'file1')) + + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() + expect(treeView.element.parentElement).toBeTruthy() expect(treeView.focus).not.toHaveBeenCalled() waitsForPromise -> treeView.focus.reset() - atom.workspace.open(path.join(atom.project.getPaths()[1], 'dir3', 'file3')) + atom.workspace.open(path.join(path2, 'dir3', 'file3')) + + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() + expect(treeView.element.parentElement).toBeTruthy() expect(treeView.focus).not.toHaveBeenCalled() + describe "if the file is located under collapsed folders", -> + it "expands all the folders and selects the file", -> + waitsForPromise -> + atom.workspace.open(path.join(path1, 'dir1', 'file1')) + + runs -> + treeView.selectEntry(root1) + treeView.collapseDirectory(true) # Recursively collapse all directories + + waitsForPromise -> + treeView.revealActiveFile() + + runs -> + expect(treeView.entryForPath(path1).classList.contains('expanded')).toBe true + expect(treeView.entryForPath(path.join(path1, 'dir1')).classList.contains('expanded')).toBe true + expect(treeView.selectedEntry()).toBeTruthy() + expect(treeView.selectedEntry().getPath()).toBe path.join(path1, 'dir1', 'file1') + describe "if the current file has no path", -> it "shows and focuses the tree view, but does not attempt to select a specific file", -> waitsForPromise -> atom.workspace.open() runs -> - expect(atom.workspace.getActivePaneItem().getPath()).toBeUndefined() - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBeUndefined() + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + + waitsForPromise -> + treeView.revealActiveFile() + + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) expect(treeView.focus).toHaveBeenCalled() describe "if there is no editor open", -> it "shows and focuses the tree view, but does not attempt to select a specific file", -> - expect(atom.workspace.getActivePaneItem()).toBeUndefined() - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') - expect(treeView.hasParent()).toBeTruthy() - expect(treeView.focus).toHaveBeenCalled() + expect(atom.workspace.getCenter().getActivePaneItem()).toBeUndefined() + expect(atom.workspace.getLeftDock().isVisible()).toBe(false) + + waitsForPromise -> + treeView.revealActiveFile() + + runs -> + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.focus).toHaveBeenCalled() describe 'if there are more items than can be visible in the viewport', -> [rootDirPath] = [] @@ -433,42 +401,51 @@ describe "TreeView", -> fs.writeFileSync(filepath, "doesn't matter") atom.project.setPaths([rootDirPath]) - treeView.height(100) + treeView.element.style.height = '100px' jasmine.attachToDOM(workspaceElement) it 'scrolls the selected file into the visible view', -> # Open file at bottom waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-20.txt')) + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') expect(treeView.scrollTop()).toBeGreaterThan 400 + entries = treeView.element.querySelectorAll('.entry') + scrollTop = treeView.element.scrollTop + for i in [0...entries.length] + atom.commands.dispatch(treeView.element, 'core:move-up') + expect(treeView.element.scrollTop - scrollTop).toBeLessThan entries[i].clientHeight + scrollTop = treeView.element.scrollTop # Open file in the middle, should be centered in scroll waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-10.txt')) + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') expect(treeView.scrollTop()).toBeLessThan 400 expect(treeView.scrollTop()).toBeGreaterThan 0 # Open file at top waitsForPromise -> atom.workspace.open(path.join(rootDirPath, 'file-1.txt')) + waitsForPromise -> + treeView.revealActiveFile() runs -> - atom.commands.dispatch(workspaceElement, 'tree-view:reveal-active-file') expect(treeView.scrollTop()).toEqual 0 - describe "when tool-panel:unfocus is triggered on the tree view", -> + describe "when tree-view:unfocus is triggered on the tree view", -> it "surrenders focus to the workspace but remains open", -> waitsForPromise -> - atom.workspace.open() # When we trigger 'tool-panel:unfocus' below, we want an editor to become focused + atom.workspace.open() # When we trigger 'tree-view:unfocus' below, we want an editor to become focused runs -> jasmine.attachToDOM(workspaceElement) treeView.focus() - expect(treeView.list).toMatchSelector(':focus') - atom.commands.dispatch(treeView.element, 'tool-panel:unfocus') - expect(treeView).toBeVisible() - expect(treeView.list).not.toMatchSelector(':focus') - expect(atom.workspace.getActivePane().isActive()).toBe(true) + expect(treeView.element).toHaveFocus() + atom.commands.dispatch(treeView.element, 'tree-view:unfocus') + expect(atom.workspace.getLeftDock().isVisible()).toBe(true) + expect(treeView.element).not.toHaveFocus() + expect(atom.workspace.getCenter().getActivePane().isActive()).toBe(true) describe "copy path commands", -> [pathToSelect, relativizedPath] = [] @@ -508,60 +485,60 @@ describe "TreeView", -> describe "when a directory's disclosure arrow is clicked", -> it "expands / collapses the associated directory", -> - subdir = root1.find('.entries > li:contains(dir1)') + subdir = root1.querySelector('.entries > li') expect(subdir).not.toHaveClass('expanded') - subdir.click() + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(subdir).toHaveClass('expanded') - subdir.click() + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(subdir).not.toHaveClass('expanded') it "restores the expansion state of descendant directories", -> - child = root1.find('.entries > li:contains(dir1)') - child.click() + child = root1.querySelector('.entries > li') + child.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - grandchild = child.find('.entries > li:contains(sub-dir1)') - grandchild.click() + grandchild = child.querySelector('.entries > li') + grandchild.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(treeView.roots[0]).not.toHaveClass('expanded') - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) # previously expanded descendants remain expanded - expect(root1.find('> .entries > li:contains(dir1) > .entries > li:contains(sub-dir1) > .entries').length).toBe 1 + expect(root1.querySelectorAll('.entries > li > .entries > li > .entries').length).toBe 1 # collapsed descendants remain collapsed - expect(root1.find('> .entries > li:contains(dir2) > .entries')).not.toHaveClass('expanded') + expect(root1.querySelectorAll('.entries > li')[1].querySelector('.entries')).not.toHaveClass('expanded') it "when collapsing a directory, removes change subscriptions from the collapsed directory and its descendants", -> - child = root1.find('li:contains(dir1)') - child.click() + child = root1.querySelector('li') + child.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - grandchild = child.find('li:contains(sub-dir1)') - grandchild.click() + grandchild = child.querySelector('li') + grandchild.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(treeView.roots[0].directory.watchSubscription).toBeTruthy() - expect(child[0].directory.watchSubscription).toBeTruthy() - expect(grandchild[0].directory.watchSubscription).toBeTruthy() + expect(root1.directory.watchSubscription).toBeTruthy() + expect(child.directory.watchSubscription).toBeTruthy() + expect(grandchild.directory.watchSubscription).toBeTruthy() - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(treeView.roots[0].directory.watchSubscription).toBeFalsy() - expect(child[0].directory.watchSubscription).toBeFalsy() - expect(grandchild[0].directory.watchSubscription).toBeFalsy() + expect(root1.directory.watchSubscription).toBeFalsy() + expect(child.directory.watchSubscription).toBeFalsy() + expect(grandchild.directory.watchSubscription).toBeFalsy() describe "when mouse down fires on a file or directory", -> it "selects the entry", -> - dir = root1.find('li:contains(dir1)') + dir = root1.querySelector('li') expect(dir).not.toHaveClass 'selected' - dir.mousedown() + dir.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, detail: 1})) expect(dir).toHaveClass 'selected' expect(sampleJs).not.toHaveClass 'selected' - sampleJs.mousedown() + sampleJs.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, detail: 1})) expect(sampleJs).toHaveClass 'selected' describe "when the package first activates and there is a file open (regression)", -> @@ -571,15 +548,20 @@ describe "TreeView", -> # UI interaction after the package was activated. describe "when the file is permanent", -> beforeEach -> - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.workspace.open('tree-view.js') it "does not throw when the file is double clicked", -> expect -> - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) - sampleJs.trigger clickEvent(originalEvent: {detail: 2}) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) .not.toThrow() + waitsFor -> + # Ensure we don't move on to the next test until the promise spawned click event resolves. + # (If it resolves in the middle of the next test we'll pollute that test). + not treeView.currentlyOpening.has(atom.workspace.getCenter().getActivePaneItem().getPath()) + describe "when the file is pending", -> editor = null @@ -590,38 +572,37 @@ describe "TreeView", -> it "marks the pending file as permanent", -> runs -> - expect(atom.workspace.getActivePane().getActiveItem()).toBe editor - expect(atom.workspace.getActivePane().getPendingItem()).toBe editor - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) - sampleJs.trigger clickEvent(originalEvent: {detail: 2}) + expect(atom.workspace.getCenter().getActivePane().getActiveItem()).toBe editor + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).toBe editor + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) waitsFor -> - atom.workspace.getActivePane().getPendingItem() is null + atom.workspace.getCenter().getActivePane().getPendingItem() is null describe "when files are clicked", -> beforeEach -> jasmine.attachToDOM(workspaceElement) describe "when a file is single-clicked", -> - describe "when core.allowPendingPaneItems is set to true (default)", -> activePaneItem = null beforeEach -> treeView.focus() - waitsForFileToOpen -> - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) + waitForWorkspaceOpenEvent -> + r = sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> - activePaneItem = atom.workspace.getActivePaneItem() + activePaneItem = atom.workspace.getCenter().getActivePaneItem() it "selects the file and retains focus on tree-view", -> expect(sampleJs).toHaveClass 'selected' - expect(treeView).toHaveFocus() + expect(treeView.element).toHaveFocus() it "opens the file in a pending state", -> expect(activePaneItem.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') - expect(atom.workspace.getActivePane().getPendingItem()).toEqual activePaneItem + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).toEqual activePaneItem describe "when core.allowPendingPaneItems is set to false", -> beforeEach -> @@ -629,11 +610,11 @@ describe "TreeView", -> spyOn(atom.workspace, 'open') treeView.focus() - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) it "selects the file and retains focus on tree-view", -> expect(sampleJs).toHaveClass 'selected' - expect(treeView).toHaveFocus() + expect(treeView.element).toHaveFocus() it "does not open the file", -> expect(atom.workspace.open).not.toHaveBeenCalled() @@ -646,14 +627,14 @@ describe "TreeView", -> spyOn(atom.workspace, 'open').andCallFake (uri, options) -> originalOpen(uri, options).then -> openedCount++ - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) treeView.openSelectedEntry() waitsFor 'open to be called twice', -> openedCount is 2 runs -> - expect(atom.workspace.getActivePane().getItems().length).toBe 1 + expect(atom.workspace.getCenter().getActivePane().getItems().length).toBe 1 describe "when a file is double-clicked", -> activePaneItem = null @@ -662,15 +643,15 @@ describe "TreeView", -> treeView.focus() it "opens the file and focuses it", -> - waitsForFileToOpen -> - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) - sampleJs.trigger clickEvent(originalEvent: {detail: 2}) + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) waitsFor "next tick to avoid race condition", (done) -> setImmediate(done) runs -> - activePaneItem = atom.workspace.getActivePaneItem() + activePaneItem = atom.workspace.getCenter().getActivePaneItem() expect(activePaneItem.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') expect(atom.views.getView(activePaneItem)).toHaveFocus() @@ -681,19 +662,19 @@ describe "TreeView", -> spyOn(atom.workspace, 'open').andCallFake (uri, options) -> originalOpen(uri, options).then -> openedCount++ - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) - sampleJs.trigger clickEvent(originalEvent: {detail: 2}) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) waitsFor 'open to be called twice', -> openedCount is 2 runs -> - expect(atom.workspace.getActivePane().getItems().length).toBe 1 + expect(atom.workspace.getCenter().getActivePane().getItems().length).toBe 1 describe "when a directory is single-clicked", -> it "is selected", -> - subdir = root1.find('.directory:first') - subdir.trigger clickEvent(originalEvent: {detail: 1}) + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(subdir).toHaveClass 'selected' describe "when a directory is double-clicked", -> @@ -701,104 +682,97 @@ describe "TreeView", -> jasmine.attachToDOM(workspaceElement) subdir = null - waitsForFileToOpen -> - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> treeView.focus() - subdir = root1.find('.directory:first') - subdir.trigger clickEvent(originalEvent: {detail: 1}) + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(subdir).toHaveClass 'selected' expect(subdir).toHaveClass 'expanded' - subdir.trigger clickEvent(originalEvent: {detail: 2}) + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) expect(subdir).toHaveClass 'selected' expect(subdir).not.toHaveClass 'expanded' - expect(treeView).toHaveFocus() + expect(treeView.element).toHaveFocus() describe "when an directory is alt-clicked", -> describe "when the directory is collapsed", -> it "recursively expands the directory", -> - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) treeView.roots[0].collapse() expect(treeView.roots[0]).not.toHaveClass 'expanded' - root1.trigger clickEvent({altKey: true}) + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1, altKey: true})) expect(treeView.roots[0]).toHaveClass 'expanded' - children = root1.find('.directory') + children = root1.querySelectorAll('.directory') expect(children.length).toBeGreaterThan 0 - children.each (index, child) -> expect(child).toHaveClass 'expanded' + for child in children + expect(child).toHaveClass 'expanded' describe "when the directory is expanded", -> parent = null children = null beforeEach -> - parent = root1.find('> .entries > .directory').eq(2) - parent[0].expand() - children = parent.find('.expanded.directory') - children.each (index, child) -> + parent = root1.querySelectorAll('.entries > .directory')[2] + parent.expand() + children = parent.querySelectorAll('.expanded.directory') + for child in children child.expand() it "recursively collapses the directory", -> - parent.click() - parent[0].expand() + parent.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + parent.expand() expect(parent).toHaveClass 'expanded' - children.each (index, child) -> - $(child).click().expand() - expect($(child)).toHaveClass 'expanded' + for child in children + child.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + child.expand() + expect(child).toHaveClass 'expanded' - parent.trigger clickEvent({altKey: true}) + parent.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1, altKey: true})) expect(parent).not.toHaveClass 'expanded' - children.each (index, child) -> + for child in children expect(child).not.toHaveClass 'expanded' expect(treeView.roots[0]).toHaveClass 'expanded' describe "when the active item changes on the active pane", -> describe "when the item has a path", -> it "selects the entry with that path in the tree view if it is visible", -> - waitsForFileToOpen -> - sampleJs.click() + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) waitsForPromise -> atom.workspace.open(atom.project.getDirectories()[0].resolve('tree-view.txt')) runs -> expect(sampleTxt).toHaveClass 'selected' - expect(treeView.find('.selected').length).toBe 1 + expect(treeView.element.querySelectorAll('.selected').length).toBe 1 it "selects the path's parent dir if its entry is not visible", -> waitsForPromise -> atom.workspace.open(path.join('dir1', 'sub-dir1', 'sub-file1')) runs -> - dirView = root1.find('.directory:contains(dir1)') + dirView = root1.querySelector('.directory') expect(dirView).toHaveClass 'selected' describe "when the tree-view.autoReveal config setting is true", -> beforeEach -> + jasmine.attachToDOM(atom.workspace.getElement()) atom.config.set "tree-view.autoReveal", true it "selects the active item's entry in the tree view, expanding parent directories if needed", -> waitsForPromise -> atom.workspace.open(path.join('dir1', 'sub-dir1', 'sub-file1')) - runs -> - dirView = root1.find('.directory:contains(dir1)') - fileView = root1.find('.file:contains(sub-file1)') - expect(dirView).not.toHaveClass 'selected' - expect(fileView).toHaveClass 'selected' - expect(treeView.find('.selected').length).toBe 1 + waitsFor -> + treeView.getSelectedEntries()[0].textContent is 'sub-file1' - describe "when the item has no path", -> - it "deselects the previously selected entry", -> - waitsForFileToOpen -> - sampleJs.click() - - runs -> - atom.workspace.getActivePane().activateItem(document.createElement("div")) - expect(treeView.find('.selected')).not.toExist() + runs -> + expect(atom.workspace.getActiveTextEditor().getElement()).toHaveFocus() describe "when a different editor becomes active", -> beforeEach -> @@ -807,15 +781,15 @@ describe "TreeView", -> it "selects the file in that is open in that editor", -> leftEditorPane = null - waitsForFileToOpen -> - sampleJs.click() + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> - leftEditorPane = atom.workspace.getActivePane() + leftEditorPane = atom.workspace.getCenter().getActivePane() leftEditorPane.splitRight() - waitsForFileToOpen -> - sampleTxt.click() + waitForWorkspaceOpenEvent -> + sampleTxt.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> expect(sampleTxt).toHaveClass('selected') @@ -824,64 +798,67 @@ describe "TreeView", -> describe "keyboard navigation", -> afterEach -> - expect(treeView.find('.selected').length).toBeLessThan 2 + expect(treeView.element.querySelectorAll('.selected').length).toBeLessThan 2 describe "core:move-down", -> describe "when a collapsed directory is selected", -> it "skips to the next directory", -> - root1.find('.directory:eq(0)').click() + root1.querySelector('.directory').dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, 'core:move-down') - expect(root1.find('.directory:eq(1)')).toHaveClass 'selected' + expect(root1.querySelectorAll('.directory')[1]).toHaveClass 'selected' describe "when an expanded directory is selected", -> it "selects the first entry of the directory", -> - subdir = root1.find('.directory:eq(1)') - subdir.click() + subdir = root1.querySelectorAll('.directory')[1] + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, 'core:move-down') - expect($(subdir[0].entries).find('.entry:first')).toHaveClass 'selected' + expect(subdir.querySelector('.entry')).toHaveClass 'selected' describe "when the last entry of an expanded directory is selected", -> it "selects the entry after its parent directory", -> - subdir1 = root1.find('.directory:eq(1)') - subdir1[0].expand() - waitsForFileToOpen -> - $(subdir1[0].entries).find('.entry:last').click() + subdir1 = root1.querySelectorAll('.directory')[1] + subdir1.expand() + waitForWorkspaceOpenEvent -> + entries = subdir1.querySelectorAll('.entry') + entries[entries.length - 1].dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'core:move-down') - expect(root1.find('.directory:eq(2)')).toHaveClass 'selected' + expect(root1.querySelectorAll('.directory')[2]).toHaveClass 'selected' describe "when the last directory of another last directory is selected", -> [nested, nested2] = [] beforeEach -> - nested = root1.find('.directory:eq(2)') - expect(nested.find('.header').text()).toContain 'nested' - nested[0].expand() - nested2 = $(nested[0].entries).find('.entry:last') - nested2.click() - nested2[0].collapse() + nested = root1.querySelectorAll('.directory')[2] + expect(nested.querySelector('.header').textContent).toContain 'nested' + nested.expand() + entries = nested.querySelectorAll('.entry') + nested2 = entries[entries.length - 1] + nested2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + nested2.collapse() describe "when the directory is collapsed", -> it "selects the entry after its grandparent directory", -> atom.commands.dispatch(treeView.element, 'core:move-down') - expect(nested.next()).toHaveClass 'selected' + expect(nested.nextSibling).toHaveClass 'selected' describe "when the directory is expanded", -> it "selects the entry after its grandparent directory", -> - nested2[0].expand() - nested2.find('.file').remove() # kill the .gitkeep file, which has to be there but screws the test + nested2.expand() + nested2.querySelector('.file').remove() # kill the .gitkeep file, which has to be there but screws the test atom.commands.dispatch(treeView.element, 'core:move-down') - expect(nested.next()).toHaveClass 'selected' + expect(nested.nextSibling).toHaveClass 'selected' describe "when the last entry of the last directory is selected", -> it "does not change the selection", -> - lastEntry = root2.find('> .entries .entry:last') - waitsForFileToOpen -> - lastEntry.click() + entries = root2.querySelectorAll('.entries .entry') + lastEntry = entries[entries.length - 1] + waitForWorkspaceOpenEvent -> + lastEntry.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'core:move-down') @@ -890,31 +867,34 @@ describe "TreeView", -> describe "core:move-up", -> describe "when there is an expanded directory before the currently selected entry", -> it "selects the last entry in the expanded directory", -> - lastDir = root1.find('.directory:last') - fileAfterDir = lastDir.next() - lastDir[0].expand() - waitsForFileToOpen -> - fileAfterDir.click() + directories = root1.querySelectorAll('.directory') + lastDir = directories[directories.length - 1] + fileAfterDir = lastDir.nextSibling + lastDir.expand() + waitForWorkspaceOpenEvent -> + fileAfterDir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'core:move-up') - expect(lastDir.find('.entry:last')).toHaveClass 'selected' + entries = lastDir.querySelectorAll('.entry') + expect(entries[entries.length - 1]).toHaveClass 'selected' describe "when there is an entry before the currently selected entry", -> it "selects the previous entry", -> - lastEntry = root1.find('.entry:last') - waitsForFileToOpen -> - lastEntry.click() + entries = root1.querySelectorAll('.entry') + lastEntry = entries[entries.length - 1] + waitForWorkspaceOpenEvent -> + lastEntry.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'core:move-up') - expect(lastEntry.prev()).toHaveClass 'selected' + expect(lastEntry.previousSibling).toHaveClass 'selected' describe "when there is no entry before the currently selected entry, but there is a parent directory", -> it "selects the parent directory", -> - subdir = root1.find('.directory:first') - subdir[0].expand() - subdir.find('> .entries > .entry:first').click() + subdir = root1.querySelector('.directory') + subdir.expand() + subdir.querySelector('.entries .entry').dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, 'core:move-up') @@ -922,7 +902,7 @@ describe "TreeView", -> describe "when there is no parent directory or previous entry", -> it "does not change the selection", -> - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, 'core:move-up') expect(treeView.roots[0]).toHaveClass 'selected' @@ -938,22 +918,19 @@ describe "TreeView", -> describe "core:move-to-top", -> it "scrolls to the top", -> - treeView.height(100) + treeView.element.style.height = '100px' jasmine.attachToDOM(treeView.element) - element.expand() for element in treeView.find('.directory') - expect(treeView.list.outerHeight()).toBeGreaterThan treeView.scroller.outerHeight() - - expect(treeView.scrollTop()).toBe 0 + element.expand() for element in treeView.element.querySelectorAll('.directory') + expect(treeView.element.scrollTop).toBe(0) - entryCount = treeView.find(".entry").length + entryCount = treeView.element.querySelectorAll(".entry").length _.times entryCount, -> atom.commands.dispatch(treeView.element, 'core:move-down') - expect(treeView.scrollTop()).toBeGreaterThan 0 atom.commands.dispatch(treeView.element, 'core:move-to-top') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe(0) it "selects the root entry", -> - entryCount = treeView.find(".entry").length + entryCount = treeView.element.querySelectorAll(".entry").length _.times entryCount, -> atom.commands.dispatch(treeView.element, 'core:move-down') expect(treeView.roots[0]).not.toHaveClass 'selected' @@ -962,76 +939,73 @@ describe "TreeView", -> describe "core:move-to-bottom", -> it "scrolls to the bottom", -> - treeView.height(100) + treeView.element.style.height = '100px' jasmine.attachToDOM(treeView.element) - element.expand() for element in treeView.find('.directory') - expect(treeView.list.outerHeight()).toBeGreaterThan treeView.scroller.outerHeight() + element.expand() for element in treeView.element.querySelectorAll('.directory') + expect(treeView.element.scrollTop).toBe(0) - expect(treeView.scrollTop()).toBe 0 atom.commands.dispatch(treeView.element, 'core:move-to-bottom') - expect(treeView.scrollBottom()).toBe root1.outerHeight() + root2.outerHeight() + expect(treeView.element.scrollTop).toBeGreaterThan(0) treeView.roots[0].collapse() treeView.roots[1].collapse() atom.commands.dispatch(treeView.element, 'core:move-to-bottom') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe(0) it "selects the last entry", -> expect(treeView.roots[0]).toHaveClass 'selected' atom.commands.dispatch(treeView.element, 'core:move-to-bottom') - expect(root2.find('.entry:last')).toHaveClass 'selected' + entries = root2.querySelectorAll('.entry') + expect(entries[entries.length - 1]).toHaveClass 'selected' describe "core:page-up", -> it "scrolls up a page", -> - treeView.height(5) + treeView.element.style.height = '5px' jasmine.attachToDOM(treeView.element) - element.expand() for element in treeView.find('.directory') - expect(treeView.list.outerHeight()).toBeGreaterThan treeView.scroller.outerHeight() + element.expand() for element in treeView.element.querySelectorAll('.directory') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe(0) treeView.scrollToBottom() - scrollTop = treeView.scrollTop() + scrollTop = treeView.element.scrollTop expect(scrollTop).toBeGreaterThan 0 atom.commands.dispatch(treeView.element, 'core:page-up') - expect(treeView.scrollTop()).toBe scrollTop - treeView.height() + expect(treeView.element.scrollTop).toBe scrollTop - treeView.element.offsetHeight describe "core:page-down", -> it "scrolls down a page", -> - treeView.height(5) + treeView.element.style.height = '5px' jasmine.attachToDOM(treeView.element) - element.expand() for element in treeView.find('.directory') - expect(treeView.list.outerHeight()).toBeGreaterThan treeView.scroller.outerHeight() + element.expand() for element in treeView.element.querySelectorAll('.directory') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe(0) atom.commands.dispatch(treeView.element, 'core:page-down') - expect(treeView.scrollTop()).toBe treeView.height() + expect(treeView.element.scrollTop).toBe treeView.element.offsetHeight describe "movement outside of viewable region", -> it "scrolls the tree view to the selected item", -> - treeView.height(100) + treeView.element.style.height = '100px' jasmine.attachToDOM(treeView.element) - element.expand() for element in treeView.find('.directory') - expect(treeView.list.outerHeight()).toBeGreaterThan treeView.scroller.outerHeight() + element.expand() for element in treeView.element.querySelectorAll('.directory') atom.commands.dispatch(treeView.element, 'core:move-down') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe(0) - entryCount = treeView.find(".entry").length - entryHeight = treeView.find('.file').height() + entryCount = treeView.element.querySelectorAll(".entry").length + entryHeight = treeView.element.querySelector('.file').offsetHeight _.times entryCount, -> atom.commands.dispatch(treeView.element, 'core:move-down') - expect(treeView.scrollBottom()).toBeGreaterThan (entryCount * entryHeight) - 1 + expect(treeView.element.scrollTop + treeView.element.offsetHeight).toBeGreaterThan((entryCount * entryHeight) - 1) _.times entryCount, -> atom.commands.dispatch(treeView.element, 'core:move-up') - expect(treeView.scrollTop()).toBe 0 + expect(treeView.element.scrollTop).toBe 0 describe "tree-view:expand-directory", -> describe "when a directory entry is selected", -> it "expands the current directory", -> - subdir = root1.find('.directory:first') - subdir.click() - subdir[0].collapse() + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.collapse() expect(subdir).not.toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:expand-item') @@ -1039,15 +1013,15 @@ describe "TreeView", -> describe "when the directory is already expanded", -> describe "when the directory is empty", -> - it "does nothing", -> + xit "does nothing", -> rootDirPath = fs.absolute(temp.mkdirSync('tree-view-root1')) fs.mkdirSync(path.join(rootDirPath, "empty-dir")) atom.project.setPaths([rootDirPath]) - rootView = $(treeView.roots[0]) + rootView = treeView.roots[0] - subdir = rootView.find('.directory:first') - subdir.click() - subdir[0].expand() + subdir = rootView.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.expand() expect(subdir).toHaveClass('expanded') expect(subdir).toHaveClass('selected') @@ -1057,17 +1031,17 @@ describe "TreeView", -> describe "when the directory has entries", -> it "moves the cursor down to the first sub-entry", -> - subdir = root1.find('.directory:first') - subdir.click() - subdir[0].expand() + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.expand() atom.commands.dispatch(treeView.element, 'tree-view:expand-item') - expect(subdir.find('.entry:first')).toHaveClass('selected') + expect(subdir.querySelector('.entry')).toHaveClass('selected') describe "when a file entry is selected", -> it "does nothing", -> - waitsForFileToOpen -> - root1.find('.file').click() + waitForWorkspaceOpenEvent -> + root1.querySelector('.file').dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'tree-view:expand-directory') @@ -1075,29 +1049,47 @@ describe "TreeView", -> describe "tree-view:recursive-expand-directory", -> describe "when an collapsed root is recursively expanded", -> it "expands the root and all subdirectories", -> - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) treeView.roots[0].collapse() expect(treeView.roots[0]).not.toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:recursive-expand-directory') expect(treeView.roots[0]).toHaveClass 'expanded' - children = root1.find('.directory') + children = root1.querySelectorAll('.directory') expect(children.length).toBeGreaterThan 0 - children.each (index, child) -> + for child in children expect(child).toHaveClass 'expanded' + describe "when a file is selected and ordered to recursively expand", -> + it "recursively expands the selected file's parent directory", -> + dir1 = root1.querySelector('.entries > .directory') + dir2 = root1.querySelectorAll('.entries > .directory')[1] + dir1.expand() + file1 = dir1.querySelector('.file') + subdir1 = dir1.querySelector('.entries > .directory') + + waitForWorkspaceOpenEvent -> + file1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + + runs -> + atom.commands.dispatch(treeView.element, 'tree-view:recursive-expand-directory') + expect(dir1).toHaveClass 'expanded' + expect(subdir1).toHaveClass 'expanded' + expect(file1).toHaveClass 'selected' + expect(dir2).toHaveClass 'collapsed' + describe "tree-view:collapse-directory", -> subdir = null beforeEach -> - subdir = root1.find('> .entries > .directory').eq(0) - subdir[0].expand() + subdir = root1.querySelector('.entries > .directory') + subdir.expand() describe "when an expanded directory is selected", -> it "collapses the selected directory", -> - subdir.click() - subdir[0].expand() + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.expand() expect(subdir).toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:collapse-directory') @@ -1107,9 +1099,9 @@ describe "TreeView", -> describe "when a collapsed directory is selected", -> it "collapses and selects the selected directory's parent directory", -> - directories = subdir.find('.directory') - directories.click() - directories[0].collapse() + directories = subdir.querySelector('.directory') + directories.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + directories.collapse() atom.commands.dispatch(treeView.element, 'tree-view:collapse-directory') expect(subdir).not.toHaveClass 'expanded' @@ -1125,8 +1117,8 @@ describe "TreeView", -> describe "when a file is selected", -> it "collapses and selects the selected file's parent directory", -> - waitsForFileToOpen -> - subdir.find('.file').click() + waitForWorkspaceOpenEvent -> + subdir.querySelector('.file').dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'tree-view:collapse-directory') @@ -1139,26 +1131,26 @@ describe "TreeView", -> children = null beforeEach -> - parent = root1.find('> .entries > .directory').eq(2) - parent[0].expand() - children = parent.find('.expanded.directory') - children.each (index, child) -> + parent = root1.querySelectorAll('.entries > .directory')[2] + parent.expand() + children = parent.querySelectorAll('.expanded.directory') + for child in children child.expand() describe "when an expanded directory is recursively collapsed", -> it "collapses the directory and all its child directories", -> - parent.click() - parent[0].expand() + parent.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + parent.expand() expect(parent).toHaveClass 'expanded' - children.each (index, child) -> - $(child).click() + for child in children + child.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) child.expand() expect(child).toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:recursive-collapse-directory') expect(parent).not.toHaveClass 'expanded' - children.each (index, child) -> + for child in children expect(child).not.toHaveClass 'expanded' expect(treeView.roots[0]).toHaveClass 'expanded' @@ -1167,50 +1159,47 @@ describe "TreeView", -> it "opens the file in the editor and focuses it", -> jasmine.attachToDOM(workspaceElement) - file = root1.find('.file:contains(tree-view.js)')[0] - treeView.selectEntry(file) + treeView.selectEntry(sampleJs) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry') runs -> - item = atom.workspace.getActivePaneItem() + item = atom.workspace.getCenter().getActivePaneItem() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') expect(atom.views.getView(item)).toHaveFocus() - expect(atom.workspace.getActivePane().getPendingItem()).not.toEqual item + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).not.toEqual item it "opens pending items in a permanent state", -> jasmine.attachToDOM(workspaceElement) - file = root1.find('.file:contains(tree-view.js)')[0] - treeView.selectEntry(file) + treeView.selectEntry(sampleJs) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch(treeView.element, 'tree-view:expand-item') runs -> - item = atom.workspace.getActivePaneItem() + item = atom.workspace.getCenter().getActivePaneItem() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') - expect(atom.workspace.getActivePane().getPendingItem()).toEqual item + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).toEqual item expect(atom.views.getView(item)).toHaveFocus() - file = root1.find('.file:contains(tree-view.js)')[0] - treeView.selectEntry(file) + treeView.selectEntry(sampleJs) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry') runs -> - item = atom.workspace.getActivePaneItem() + item = atom.workspace.getCenter().getActivePaneItem() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') expect(atom.views.getView(item)).toHaveFocus() - expect(atom.workspace.getActivePane().getPendingItem()).not.toEqual item + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).not.toEqual item describe "when a directory is selected", -> it "expands or collapses the directory", -> - subdir = root1.find('.directory').first() - subdir.click() - subdir[0].collapse() + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.collapse() expect(subdir).not.toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry') @@ -1221,7 +1210,7 @@ describe "TreeView", -> describe "when nothing is selected", -> it "does nothing", -> atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry') - expect(atom.workspace.getActivePaneItem()).toBeUndefined() + expect(atom.workspace.getCenter().getActivePaneItem()).toBeUndefined() describe "opening in new split panes", -> splitOptions = @@ -1240,14 +1229,14 @@ describe "TreeView", -> beforeEach -> jasmine.attachToDOM(workspaceElement) - waitsForFileToOpen -> - root1.find('.file:contains(tree-view.js)').click() + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> - previousPane = atom.workspace.getActivePane() + previousPane = atom.workspace.getCenter().getActivePane() spyOn(previousPane, 'split').andCallThrough() - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> selectEntry 'tree-view.txt' atom.commands.dispatch(treeView.element, command) @@ -1255,8 +1244,8 @@ describe "TreeView", -> expect(previousPane.split).toHaveBeenCalledWith options... it "opens the file in the new split pane and focuses it", -> - splitPane = atom.workspace.getActivePane() - splitPaneItem = atom.workspace.getActivePaneItem() + splitPane = atom.workspace.getCenter().getActivePane() + splitPaneItem = atom.workspace.getCenter().getActivePaneItem() expect(previousPane).not.toBe splitPane expect(splitPaneItem.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') expect(atom.views.getView(splitPaneItem)).toHaveFocus() @@ -1264,35 +1253,34 @@ describe "TreeView", -> describe "when a directory is selected", -> it "does nothing", -> atom.commands.dispatch(treeView.element, command) - expect(atom.workspace.getActivePaneItem()).toBeUndefined() + expect(atom.workspace.getCenter().getActivePaneItem()).toBeUndefined() describe "when nothing is selected", -> it "does nothing", -> atom.commands.dispatch(treeView.element, command) - expect(atom.workspace.getActivePaneItem()).toBeUndefined() + expect(atom.workspace.getCenter().getActivePaneItem()).toBeUndefined() describe "tree-view:expand-item", -> describe "when a file is selected", -> it "opens the file in the editor in pending state and focuses it", -> jasmine.attachToDOM(workspaceElement) - file = root1.find('.file:contains(tree-view.js)')[0] - treeView.selectEntry(file) + treeView.selectEntry(sampleJs) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch(treeView.element, 'tree-view:expand-item') runs -> - item = atom.workspace.getActivePaneItem() + item = atom.workspace.getCenter().getActivePaneItem() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') - expect(atom.workspace.getActivePane().getPendingItem()).toEqual item + expect(atom.workspace.getCenter().getActivePane().getPendingItem()).toEqual item expect(atom.views.getView(item)).toHaveFocus() describe "when a directory is selected", -> it "expands the directory", -> - subdir = root1.find('.directory').first() - subdir.click() - subdir[0].collapse() + subdir = root1.querySelector('.directory') + subdir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + subdir.collapse() expect(subdir).not.toHaveClass 'expanded' atom.commands.dispatch(treeView.element, 'tree-view:expand-item') @@ -1301,18 +1289,18 @@ describe "TreeView", -> describe "when nothing is selected", -> it "does nothing", -> atom.commands.dispatch(treeView.element, 'tree-view:expand-item') - expect(atom.workspace.getActivePaneItem()).toBeUndefined() + expect(atom.workspace.getCenter().getActivePaneItem()).toBeUndefined() describe "opening in existing split panes", -> beforeEach -> jasmine.attachToDOM(workspaceElement) [1..9].forEach -> - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> selectEntry "tree-view.js" atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right') it "should have opened all windows", -> - expect(atom.workspace.getPanes().length).toBe 9 + expect(atom.workspace.getCenter().getPanes().length).toBe 9 [0..8].forEach (index) -> paneNumber = index + 1 @@ -1322,12 +1310,12 @@ describe "TreeView", -> describe "when a file is selected", -> beforeEach -> selectEntry 'tree-view.txt' - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch treeView.element, command it "opens the file in pane #{paneNumber} and focuses it", -> - pane = atom.workspace.getPanes()[index] - item = atom.workspace.getActivePaneItem() + pane = atom.workspace.getCenter().getPanes()[index] + item = atom.workspace.getCenter().getActivePaneItem() expect(atom.views.getView(pane)).toHaveFocus() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') @@ -1337,13 +1325,14 @@ describe "TreeView", -> atom.project.setPaths([projectPath]) jasmine.attachToDOM(workspaceElement) + global.debug = true [1..9].forEach (index) -> - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> selectEntry getPaneFileName(index) atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right') it "should have opened all windows", -> - expect(atom.workspace.getPanes().length).toBe 9 + expect(atom.workspace.getCenter().getPanes().length).toBe 9 [0..8].forEach (index) -> paneNumber = index + 1 @@ -1355,20 +1344,39 @@ describe "TreeView", -> describe "when a file is selected that is already open in pane #{fileIndex}", -> beforeEach -> selectEntry fileName - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch treeView.element, command it "opens the file in pane #{paneNumber} and focuses it", -> - pane = atom.workspace.getPanes()[index] - item = atom.workspace.getActivePaneItem() + pane = atom.workspace.getCenter().getPanes()[index] + item = atom.workspace.getCenter().getActivePaneItem() expect(atom.views.getView(pane)).toHaveFocus() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve(fileName) describe "removing a project folder", -> - it "removes the folder from the project", -> - rootHeader = treeView.roots[1].querySelector(".header") - atom.commands.dispatch(rootHeader, "tree-view:remove-project-folder") - expect(atom.project.getPaths()).toHaveLength(1) + describe "when the project folder is selected", -> + it "removes the folder from the project", -> + rootHeader = treeView.roots[1].querySelector(".header") + atom.commands.dispatch(rootHeader, "tree-view:remove-project-folder") + expect(atom.project.getPaths()).toEqual [path1] + + describe "when an entry is selected", -> + it "removes the project folder containing the entry", -> + treeView.selectEntry(treeView.roots[1].querySelector(".entries").querySelector("li")) + atom.commands.dispatch(treeView.element, "tree-view:remove-project-folder") + expect(atom.project.getPaths()).toEqual [path1] + + describe "when nothing is selected and there is only one project folder", -> + it "removes the project folder", -> + atom.project.removePath(path2) + atom.commands.dispatch(treeView.element, "tree-view:remove-project-folder") + expect(atom.project.getPaths()).toHaveLength 0 + + describe "when nothing is selected and there are multiple project folders", -> + it "does nothing", -> + treeView.deselect(treeView.getSelectedEntries()) + atom.commands.dispatch(treeView.element, "tree-view:remove-project-folder") + expect(atom.project.getPaths()).toHaveLength 2 describe "file modification", -> [dirView, dirView2, dirView3, fileView, fileView2, fileView3, fileView4] = [] @@ -1400,26 +1408,23 @@ describe "TreeView", -> atom.project.setPaths([rootDirPath, rootDirPath2]) - root1 = $(treeView.roots[0]) - dirView = $(treeView.roots[0].entries).find('.directory:contains(test-dir):first') - dirView[0].expand() - fileView = treeView.find('.file:contains(test-file.txt)') - dirView2 = $(treeView.roots[0].entries).find('.directory:contains(test-dir2):last') - dirView2[0].expand() - fileView2 = treeView.find('.file:contains(test-file2.txt)') - fileView3 = treeView.find('.file:contains(test-file3.txt)') - dirView3 = $(treeView.roots[1].entries).find('.directory:contains(test-dir3):first') - dirView3[0].expand() - fileView4 = treeView.find('.file:contains(test-file4.txt)') - fileView5 = treeView.find('.file:contains(test-file5.txt)') + root1 = treeView.roots[0] + root2 = treeView.roots[1] + [dirView, dirView2] = root1.querySelectorAll('.directory') + dirView.expand() + dirView2.expand() + dirView3 = root2.querySelector('.directory') + dirView3.expand() + [fileView, fileView2, fileView3] = root1.querySelectorAll('.file') + fileView4 = root2.querySelector('.file') describe "tree-view:copy", -> LocalStorage = window.localStorage beforeEach -> LocalStorage.clear() - waitsForFileToOpen -> - fileView2.click() + waitForWorkspaceOpenEvent -> + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:copy") @@ -1436,13 +1441,13 @@ describe "TreeView", -> describe 'when multiple files are selected', -> it 'saves the selected item paths in localStorage', -> - fileView3.addClass('selected') + fileView3.classList.add('selected') atom.commands.dispatch(treeView.element, "tree-view:copy") storedPaths = JSON.parse(LocalStorage['tree-view:copyPath']) expect(storedPaths.length).toBe 2 - expect(storedPaths[0]).toBe fileView2[0].getPath() - expect(storedPaths[1]).toBe fileView3[0].getPath() + expect(storedPaths[0]).toBe fileView2.getPath() + expect(storedPaths[1]).toBe fileView3.getPath() describe "tree-view:cut", -> LocalStorage = window.localStorage @@ -1450,8 +1455,8 @@ describe "TreeView", -> beforeEach -> LocalStorage.clear() - waitsForFileToOpen -> - fileView2.click() + waitForWorkspaceOpenEvent -> + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:cut") @@ -1469,13 +1474,13 @@ describe "TreeView", -> describe 'when multiple files are selected', -> it 'saves the selected item paths in localStorage', -> LocalStorage.clear() - fileView3.addClass('selected') + fileView3.classList.add('selected') atom.commands.dispatch(treeView.element, "tree-view:cut") storedPaths = JSON.parse(LocalStorage['tree-view:cutPath']) expect(storedPaths.length).toBe 2 - expect(storedPaths[0]).toBe fileView2[0].getPath() - expect(storedPaths[1]).toBe fileView3[0].getPath() + expect(storedPaths[0]).toBe fileView2.getPath() + expect(storedPaths[1]).toBe fileView3.getPath() describe "tree-view:paste", -> LocalStorage = window.localStorage @@ -1485,18 +1490,27 @@ describe "TreeView", -> describe "when attempting to paste a directory into itself", -> describe "when copied", -> - it "makes a copy inside itself", -> + beforeEach -> LocalStorage['tree-view:copyPath'] = JSON.stringify([dirPath]) - dirView.click() - + it "makes a copy inside itself", -> newPath = path.join(dirPath, path.basename(dirPath)) + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() expect(fs.existsSync(newPath)).toBeTruthy() + it "dispatches an event to the tree-view", -> + newPath = path.join(dirPath, path.basename(dirPath)) + callback = jasmine.createSpy("onEntryCopied") + treeView.onEntryCopied(callback) + + dirView.click() + atom.commands.dispatch(treeView.element, "tree-view:paste") + expect(callback).toHaveBeenCalledWith(initialPath: dirPath, newPath: newPath) + it 'does not keep copying recursively', -> LocalStorage['tree-view:copyPath'] = JSON.stringify([dirPath]) - dirView.click() + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) newPath = path.join(dirPath, path.basename(dirPath)) expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() @@ -1506,7 +1520,7 @@ describe "TreeView", -> describe "when cut", -> it "does nothing", -> LocalStorage['tree-view:cutPath'] = JSON.stringify([dirPath]) - dirView.click() + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) expect(fs.existsSync(dirPath)).toBeTruthy() expect(fs.existsSync(path.join(dirPath, path.basename(dirPath)))).toBeFalsy() @@ -1518,7 +1532,7 @@ describe "TreeView", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath2, filePathDoesntExist1, filePath3, filePathDoesntExist2]) - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath, path.basename(filePath2)))).toBeTruthy() @@ -1530,20 +1544,31 @@ describe "TreeView", -> describe "when a file has been copied", -> describe "when a file is selected", -> - it "creates a copy of the original file in the selected file's parent directory", -> + beforeEach -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - fileView2.click() + it "creates a copy of the original file in the selected file's parent directory", -> + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy() + newPath = path.join(dirPath2, path.basename(filePath)) + expect(fs.existsSync(newPath)).toBeTruthy() expect(fs.existsSync(filePath)).toBeTruthy() + it "emits an event", -> + callback = jasmine.createSpy("onEntryCopied") + treeView.onEntryCopied(callback) + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + newPath = path.join(dirPath2, path.basename(filePath)) + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath: newPath}) + describe "when the target already exists", -> it "appends a number to the destination name", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") atom.commands.dispatch(treeView.element, "tree-view:paste") @@ -1558,10 +1583,9 @@ describe "TreeView", -> fs.writeFileSync(dotFilePath, "doesn't matter .") LocalStorage['tree-view:copyPath'] = JSON.stringify([dotFilePath]) - treeView.find('.file:contains(test.file.txt)').click() atom.commands.dispatch(treeView.element, "tree-view:paste") - fileView2.click() + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath, path.basename(dotFilePath)))).toBeTruthy() expect(fs.existsSync(dotFilePath)).toBeTruthy() @@ -1572,7 +1596,7 @@ describe "TreeView", -> fs.writeFileSync(dotFilePath, "doesn't matter .") LocalStorage['tree-view:copyPath'] = JSON.stringify([dotFilePath]) - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") atom.commands.dispatch(treeView.element, "tree-view:paste") @@ -1584,7 +1608,7 @@ describe "TreeView", -> it "creates a copy of the original file in the selected directory", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - dirView2.click() + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy() @@ -1594,7 +1618,7 @@ describe "TreeView", -> it "appends a number to the destination file name", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - dirView.click() + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") atom.commands.dispatch(treeView.element, "tree-view:paste") @@ -1614,8 +1638,9 @@ describe "TreeView", -> it "creates a copy of the original file in the selected directory", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - dotDirView = $(treeView.roots[0].entries).find('.directory:contains(test\\.dir)') - dotDirView.click() + directories = treeView.roots[0].entries.querySelectorAll('.directory') + dotDirView = directories[directories.length - 1] + dotDirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dotDirPath, path.basename(filePath)))).toBeTruthy() @@ -1627,8 +1652,9 @@ describe "TreeView", -> fs.writeFileSync(dotFilePath, "doesn't matter .") LocalStorage['tree-view:copyPath'] = JSON.stringify([dotFilePath]) - dotDirView = $(treeView.roots[0].entries).find('.directory:contains(test\\.dir)') - dotDirView.click() + directories = treeView.roots[0].entries.querySelectorAll('.directory') + dotDirView = directories[directories.length - 1] + dotDirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") atom.commands.dispatch(treeView.element, "tree-view:paste") @@ -1639,7 +1665,7 @@ describe "TreeView", -> describe "when pasting into a different root directory", -> it "creates the file", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath4]) - dirView2.click() + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath2, path.basename(filePath4)))).toBeTruthy() @@ -1651,7 +1677,7 @@ describe "TreeView", -> asteriskFilePath = path.join(dirPath, "test-file-**.txt") fs.writeFileSync(asteriskFilePath, "doesn't matter *") LocalStorage['tree-view:copyPath'] = JSON.stringify([asteriskFilePath]) - dirView2.click() + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath2, path.basename(asteriskFilePath)))).toBeTruthy() @@ -1664,7 +1690,7 @@ describe "TreeView", -> it "copies the selected files to the parent directory of the selected file", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath2, filePath3]) - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath, path.basename(filePath2)))).toBeTruthy() @@ -1681,58 +1707,91 @@ describe "TreeView", -> fs.writeFileSync(filePath4, "doesn't matter") fs.writeFileSync(filePath5, "doesn't matter") - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath, "test-file20.txt"))).toBeTruthy() expect(fs.existsSync(path.join(dirPath, "test-file30.txt"))).toBeTruthy() describe "when a file has been cut", -> + beforeEach -> + LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath]) + describe "when a file is selected", -> it "creates a copy of the original file in the selected file's parent directory and removes the original", -> - LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath]) - - fileView2.click() + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy() + newPath = path.join(dirPath2, path.basename(filePath)) + expect(fs.existsSync(newPath)).toBeTruthy() expect(fs.existsSync(filePath)).toBeFalsy() + it "emits an event", -> + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + newPath = path.join(dirPath2, path.basename(filePath)) + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath}) + describe 'when the target destination file exists', -> it 'does not move the cut file', -> - LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath]) + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) filePath3 = path.join(dirPath2, "test-file.txt") fs.writeFileSync(filePath3, "doesn't matter") - fileView2.click() + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(filePath)).toBeTruthy() + expect(callback).not.toHaveBeenCalled() describe "when a directory is selected", -> it "creates a copy of the original file in the selected directory and removes the original", -> LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath]) - dirView2.click() + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy() + newPath = path.join(dirPath2, path.basename(filePath)) + expect(fs.existsSync(newPath)).toBeTruthy() expect(fs.existsSync(filePath)).toBeFalsy() + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath}) describe "when multiple files have been cut", -> describe "when a file is selected", -> - it "moves the selected files to the parent directory of the selected file", -> + beforeEach -> LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath2, filePath3]) - fileView.click() + it "moves the selected files to the parent directory of the selected file", -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(fs.existsSync(path.join(dirPath, path.basename(filePath2)))).toBeTruthy() - expect(fs.existsSync(path.join(dirPath, path.basename(filePath3)))).toBeTruthy() + newPath2 = path.join(dirPath, path.basename(filePath2)) + newPath3 = path.join(dirPath, path.basename(filePath3)) + expect(fs.existsSync(newPath2)).toBeTruthy() + expect(fs.existsSync(newPath3)).toBeTruthy() expect(fs.existsSync(filePath2)).toBeFalsy() expect(fs.existsSync(filePath3)).toBeFalsy() + it "emits events", -> + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + newPath2 = path.join(dirPath, path.basename(filePath2)) + newPath3 = path.join(dirPath, path.basename(filePath3)) + expect(callback.callCount).toEqual(2) + expect(callback).toHaveBeenCalledWith({initialPath: filePath2, newPath: newPath2}) + expect(callback).toHaveBeenCalledWith({initialPath: filePath3, newPath: newPath3}) + describe 'when the target destination file exists', -> it 'does not move the cut file', -> LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath2, filePath3]) @@ -1742,7 +1801,7 @@ describe "TreeView", -> fs.writeFileSync(filePath4, "doesn't matter") fs.writeFileSync(filePath5, "doesn't matter") - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(filePath2)).toBeTruthy() @@ -1752,7 +1811,7 @@ describe "TreeView", -> it "creates a copy of the original file in the selected directory and removes the original", -> LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath]) - dirView2.click() + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") expect(fs.existsSync(path.join(dirPath2, path.basename(filePath)))).toBeTruthy() @@ -1768,7 +1827,7 @@ describe "TreeView", -> LocalStorage['tree-view:copyPath'] = JSON.stringify([filePath]) - fileView2.click() + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.notifications.clear() atom.commands.dispatch(treeView.element, "tree-view:paste") @@ -1776,171 +1835,186 @@ describe "TreeView", -> expect(atom.notifications.getNotifications()[0].getDetail()).toContain 'ENOENT: no such file or directory' describe "tree-view:add-file", -> - [addPanel, addDialog] = [] + [addPanel, addDialog, callback] = [] beforeEach -> jasmine.attachToDOM(workspaceElement) + callback = jasmine.createSpy("onFileCreated") + treeView.onFileCreated(callback) - waitsForFileToOpen -> - fileView.click() + waitForWorkspaceOpenEvent -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:add-file") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() + addDialog = addPanel.getItem() describe "when a file is selected", -> it "opens an add dialog with the file's current directory path populated", -> - expect(addDialog).toExist() - expect(addDialog.promptText.text()).toBeTruthy() + expect(addDialog.element).toExist() + expect(addDialog.promptText.textContent).toBeTruthy() expect(atom.project.relativize(dirPath)).toMatch(/[^\\\/]$/) expect(addDialog.miniEditor.getText()).toBe(atom.project.relativize(dirPath) + path.sep) - expect(addDialog.miniEditor.getModel().getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length - expect(addDialog.miniEditor).toHaveFocus() + expect(addDialog.miniEditor.getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length + expect(addDialog.miniEditor.element).toHaveFocus() describe "when the parent directory of the selected file changes", -> it "still shows the active file as selected", -> - dirView[0].directory.emitter.emit 'did-remove-entries', {'deleted.txt': {}} - expect(treeView.find('.selected').text()).toBe path.basename(filePath) + dirView.directory.emitter.emit 'did-remove-entries', new Map().set('deleted.txt', {}) + expect(treeView.element.querySelector('.selected').textContent).toBe path.basename(filePath) describe "when the path without a trailing '#{path.sep}' is changed and confirmed", -> describe "when no file exists at that location", -> - it "add a file, closes the dialog and selects the file in the tree-view", -> + it "adds a file, closes the dialog, selects the file in the tree-view, and emits an event", -> newPath = path.join(dirPath, "new-test-file.txt") - waitsForFileToOpen -> - addDialog.miniEditor.getModel().insertText(path.basename(newPath)) + waitForWorkspaceOpenEvent -> + addDialog.miniEditor.insertText(path.basename(newPath)) atom.commands.dispatch addDialog.element, 'core:confirm' runs -> expect(fs.isFileSync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).toBe newPath + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe newPath + + waitsFor "file to be added to tree view", -> + dirView.entries.querySelectorAll(".file").length > 1 - waitsFor "tree view to be updated", -> - $(dirView[0].entries).find("> .file").length > 1 + waitsFor "tree view selection to be updated", -> + treeView.element.querySelector('.file.selected') isnt null runs -> - expect(treeView.find('.selected').text()).toBe path.basename(newPath) + expect(treeView.element.querySelector('.selected').textContent).toBe path.basename(newPath) + expect(callback).toHaveBeenCalledWith({path: newPath}) it "adds file in any project path", -> newPath = path.join(dirPath3, "new-test-file.txt") - waitsForFileToOpen -> - fileView4.click() + waitForWorkspaceOpenEvent -> + fileView4.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch(treeView.element, "tree-view:add-file") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() - addDialog.miniEditor.getModel().insertText(path.basename(newPath)) + addDialog = addPanel.getItem() + addDialog.miniEditor.insertText(path.basename(newPath)) atom.commands.dispatch addDialog.element, 'core:confirm' runs -> expect(fs.isFileSync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).toBe newPath + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe newPath + + waitsFor "file to be added to tree view", -> + dirView3.entries.querySelectorAll(".file").length > 1 - waitsFor "tree view to be updated", -> - $(dirView3[0].entries).find("> .file").length > 1 + waitsFor "tree view selection to be updated", -> + treeView.element.querySelector('.file.selected') isnt null runs -> - expect(treeView.find('.selected').text()).toBe path.basename(newPath) + expect(treeView.element.querySelector('.selected').textContent).toBe path.basename(newPath) + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when a file already exists at that location", -> it "shows an error message and does not close the dialog", -> newPath = path.join(dirPath, "new-test-file.txt") fs.writeFileSync(newPath, '') - addDialog.miniEditor.getModel().insertText(path.basename(newPath)) + addDialog.miniEditor.insertText(path.basename(newPath)) atom.commands.dispatch addDialog.element, 'core:confirm' - expect(addDialog.errorMessage.text()).toContain 'already exists' - expect(addDialog).toHaveClass('error') + expect(addDialog.errorMessage.textContent).toContain 'already exists' + expect(addDialog.element).toHaveClass('error') expect(atom.workspace.getModalPanels()[0]).toBe addPanel + expect(callback).not.toHaveBeenCalled() describe "when the project has no path", -> - it "add a file and closes the dialog", -> + it "adds a file and closes the dialog", -> atom.project.setPaths([]) addDialog.close() - atom.commands.dispatch(treeView.element, "tree-view:add-file") + atom.commands.dispatch(atom.views.getView(atom.workspace), "tree-view:add-file") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() + addDialog = addPanel.getItem() - newPath = temp.path() - addDialog.miniEditor.getModel().insertText(newPath) + newPath = path.join(fs.realpathSync(temp.mkdirSync()), 'a-file') + addDialog.miniEditor.insertText(newPath) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch addDialog.element, 'core:confirm' runs -> expect(fs.isFileSync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).toBe fs.realpathSync(newPath) + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe(newPath) + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when the path with a trailing '#{path.sep}' is changed and confirmed", -> it "shows an error message and does not close the dialog", -> - addDialog.miniEditor.getModel().insertText("new-test-file" + path.sep) + addDialog.miniEditor.insertText("new-test-file" + path.sep) atom.commands.dispatch addDialog.element, 'core:confirm' - expect(addDialog.errorMessage.text()).toContain 'names must not end with' - expect(addDialog).toHaveClass('error') + expect(addDialog.errorMessage.textContent).toContain 'names must not end with' + expect(addDialog.element).toHaveClass('error') expect(atom.workspace.getModalPanels()[0]).toBe addPanel + expect(callback).not.toHaveBeenCalled() describe "when 'core:cancel' is triggered on the add dialog", -> it "removes the dialog and focuses the tree view", -> atom.commands.dispatch addDialog.element, 'core:cancel' expect(atom.workspace.getModalPanels().length).toBe 0 - expect(treeView.find(".tree-view")).toMatchSelector(':focus') + expect(document.activeElement).toBe(treeView.element) + expect(callback).not.toHaveBeenCalled() describe "when the add dialog's editor loses focus", -> it "removes the dialog and focuses root view", -> workspaceElement.focus() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus() + expect(atom.views.getView(atom.workspace.getCenter().getActivePane())).toHaveFocus() describe "when the path ends with whitespace", -> it "removes the trailing whitespace before creating the file", -> newPath = path.join(dirPath, "new-test-file.txt") - addDialog.miniEditor.getModel().insertText(path.basename(newPath) + " ") + addDialog.miniEditor.insertText(path.basename(newPath) + " ") - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch addDialog.element, 'core:confirm' runs -> expect(fs.isFileSync(newPath)).toBeTruthy() - expect(atom.workspace.getActivePaneItem().getPath()).toBe newPath + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe newPath + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when a directory is selected", -> it "opens an add dialog with the directory's path populated", -> addDialog.cancel() - dirView.click() + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:add-file") - addDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + addDialog = atom.workspace.getModalPanels()[0].getItem() - expect(addDialog).toExist() - expect(addDialog.promptText.text()).toBeTruthy() + expect(addDialog.element).toExist() + expect(addDialog.promptText.textContent).toBeTruthy() expect(atom.project.relativize(dirPath)).toMatch(/[^\\\/]$/) expect(addDialog.miniEditor.getText()).toBe(atom.project.relativize(dirPath) + path.sep) - expect(addDialog.miniEditor.getModel().getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length - expect(addDialog.miniEditor).toHaveFocus() + expect(addDialog.miniEditor.getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length + expect(addDialog.miniEditor.element).toHaveFocus() describe "when the root directory is selected", -> it "opens an add dialog with no path populated", -> addDialog.cancel() - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:add-file") - addDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + addDialog = atom.workspace.getModalPanels()[0].getItem() expect(addDialog.miniEditor.getText()).toBe "" describe "when there is no entry selected", -> it "opens an add dialog with no path populated", -> addDialog.cancel() - root1.click() - root1.removeClass('selected') + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + root1.classList.remove('selected') expect(treeView.selectedEntry()).toBeNull() atom.commands.dispatch(treeView.element, "tree-view:add-file") - addDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + addDialog = atom.workspace.getModalPanels()[0].getItem() expect(addDialog.miniEditor.getText()).toBe "" @@ -1950,57 +2024,63 @@ describe "TreeView", -> atom.project.setPaths([]) atom.commands.dispatch(workspaceElement, "tree-view:add-folder") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() - addDialog.miniEditor.getModel().insertText("a-file") + addDialog = addPanel.getItem() + addDialog.miniEditor.insertText("a-file") atom.commands.dispatch(addDialog.element, 'core:confirm') - expect(addDialog.text()).toContain("You must open a directory to create a file with a relative path") + expect(addDialog.element.textContent).toContain("You must open a directory to create a file with a relative path") describe "tree-view:add-folder", -> - [addPanel, addDialog] = [] + [addPanel, addDialog, callback] = [] beforeEach -> jasmine.attachToDOM(workspaceElement) + callback = jasmine.createSpy("onDirectoryCreated") + treeView.onDirectoryCreated(callback) - waitsForFileToOpen -> - fileView.click() + waitForWorkspaceOpenEvent -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:add-folder") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() + addDialog = addPanel.getItem() describe "when a file is selected", -> it "opens an add dialog with the file's current directory path populated", -> - expect(addDialog).toExist() - expect(addDialog.promptText.text()).toBeTruthy() + expect(addDialog.element).toExist() + expect(addDialog.promptText.textContent).toBeTruthy() expect(atom.project.relativize(dirPath)).toMatch(/[^\\\/]$/) expect(addDialog.miniEditor.getText()).toBe(atom.project.relativize(dirPath) + path.sep) - expect(addDialog.miniEditor.getModel().getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length - expect(addDialog.miniEditor).toHaveFocus() + expect(addDialog.miniEditor.getCursorBufferPosition().column).toBe addDialog.miniEditor.getText().length + expect(addDialog.miniEditor.element).toHaveFocus() describe "when the path without a trailing '#{path.sep}' is changed and confirmed", -> describe "when no directory exists at the given path", -> it "adds a directory and closes the dialog", -> newPath = path.join(dirPath, 'new', 'dir') - addDialog.miniEditor.getModel().insertText("new#{path.sep}dir") + addDialog.miniEditor.insertText("new#{path.sep}dir") atom.commands.dispatch addDialog.element, 'core:confirm' expect(fs.isDirectorySync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath - expect(treeView.find(".tree-view")).toMatchSelector(':focus') - expect(dirView.find('.directory.selected:contains(new)').length).toBe 1 + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).not.toBe newPath + + expect(document.activeElement).toBe(treeView.element) + expect(dirView.querySelector('.directory.selected').textContent).toBe('new') + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when the path with a trailing '#{path.sep}' is changed and confirmed", -> describe "when no directory exists at the given path", -> - it "adds a directory and closes the dialog", -> + it "adds a directory, closes the dialog, and emits an event", -> newPath = path.join(dirPath, 'new', 'dir') - addDialog.miniEditor.getModel().insertText("new#{path.sep}dir#{path.sep}") + addDialog.miniEditor.insertText("new#{path.sep}dir#{path.sep}") atom.commands.dispatch addDialog.element, 'core:confirm' expect(fs.isDirectorySync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath - expect(treeView.find(".tree-view")).toMatchSelector(':focus') - expect(dirView.find('.directory.selected:contains(new)').length).toBe(1) + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).not.toBe newPath + + expect(document.activeElement).toBe(treeView.element) + expect(dirView.querySelector('.directory.selected').textContent).toBe('new') + expect(callback).toHaveBeenCalledWith({path: newPath + path.sep}) it "selects the created directory and does not change the expansion state of existing directories", -> expandedPath = path.join(dirPath, 'expanded-dir') @@ -2011,54 +2091,60 @@ describe "TreeView", -> expandedView.expand() newPath = path.join(dirPath, "new2") + path.sep - addDialog.miniEditor.getModel().insertText("new2#{path.sep}") + addDialog.miniEditor.insertText("new2#{path.sep}") atom.commands.dispatch addDialog.element, 'core:confirm' expect(fs.isDirectorySync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.workspace.getActivePaneItem().getPath()).not.toBe newPath - expect(treeView.find(".tree-view")).toMatchSelector(':focus') - expect(dirView.find('.directory.selected:contains(new2)').length).toBe(1) + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).not.toBe newPath + + expect(document.activeElement).toBe(treeView.element) + expect(dirView.querySelector('.directory.selected').textContent).toBe('new2') expect(treeView.entryForPath(expandedPath).isExpanded).toBeTruthy() + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when the project has no path", -> it "adds a directory and closes the dialog", -> addDialog.close() atom.project.setPaths([]) - atom.commands.dispatch(treeView.element, "tree-view:add-folder") + atom.commands.dispatch(atom.views.getView(atom.workspace), "tree-view:add-folder") [addPanel] = atom.workspace.getModalPanels() - addDialog = $(addPanel.getItem()).view() + addDialog = addPanel.getItem() - expect(addDialog.miniEditor.getModel().getText()).toBe '' + expect(addDialog.miniEditor.getText()).toBe '' newPath = temp.path() - addDialog.miniEditor.getModel().insertText(newPath) + addDialog.miniEditor.insertText(newPath) atom.commands.dispatch addDialog.element, 'core:confirm' expect(fs.isDirectorySync(newPath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 + expect(callback).toHaveBeenCalledWith({path: newPath}) describe "when a directory already exists at the given path", -> it "shows an error message and does not close the dialog", -> newPath = path.join(dirPath, "new-dir") fs.makeTreeSync(newPath) - addDialog.miniEditor.getModel().insertText("new-dir#{path.sep}") + addDialog.miniEditor.insertText("new-dir#{path.sep}") atom.commands.dispatch addDialog.element, 'core:confirm' - expect(addDialog.errorMessage.text()).toContain 'already exists' - expect(addDialog).toHaveClass('error') + expect(addDialog.errorMessage.textContent).toContain 'already exists' + expect(addDialog.element).toHaveClass('error') expect(atom.workspace.getModalPanels()[0]).toBe addPanel + expect(callback).not.toHaveBeenCalled() describe "tree-view:move", -> describe "when a file is selected", -> - moveDialog = null + [moveDialog, callback] = [] beforeEach -> jasmine.attachToDOM(workspaceElement) + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) - waitsForFileToOpen -> - fileView.click() + waitForWorkspaceOpenEvent -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:move") - moveDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + moveDialog = atom.workspace.getModalPanels()[0].getItem() afterEach -> waits 50 # The move specs cause too many false positives because of their async nature, so wait a little bit before we cleanup @@ -2066,15 +2152,15 @@ describe "TreeView", -> it "opens a move dialog with the file's current path (excluding extension) populated", -> extension = path.extname(filePath) fileNameWithoutExtension = path.basename(filePath, extension) - expect(moveDialog).toExist() - expect(moveDialog.promptText.text()).toBe "Enter the new path for the file." + expect(moveDialog.element).toExist() + expect(moveDialog.promptText.textContent).toBe "Enter the new path for the file." expect(moveDialog.miniEditor.getText()).toBe(atom.project.relativize(filePath)) - expect(moveDialog.miniEditor.getModel().getSelectedText()).toBe path.basename(fileNameWithoutExtension) - expect(moveDialog.miniEditor).toHaveFocus() + expect(moveDialog.miniEditor.getSelectedText()).toBe fileNameWithoutExtension + expect(moveDialog.miniEditor.element).toHaveFocus() describe "when the path is changed and confirmed", -> describe "when all the directories along the new path exist", -> - it "moves the file, updates the tree view, and closes the dialog", -> + it "moves the file, updates the tree view, closes the dialog, and emits an event", -> newPath = path.join(rootDirPath, 'renamed-test-file.txt') moveDialog.miniEditor.setText(path.basename(newPath)) @@ -2085,12 +2171,14 @@ describe "TreeView", -> expect(atom.workspace.getModalPanels().length).toBe 0 waitsFor "tree view to update", -> - root1.find('> .entries > .file:contains(renamed-test-file.txt)').length > 0 + files = Array.from(root1.querySelectorAll('.entries .file')) + files.filter((f) -> f.textContent is 'renamed-test-file.txt').length > 0 runs -> - dirView = $(treeView.roots[0].entries).find('.directory:contains(test-dir)') - dirView[0].expand() - expect($(dirView[0].entries).children().length).toBe 0 + dirView = treeView.roots[0].querySelector('.directory') + dirView.expand() + expect(dirView.entries.children.length).toBe 0 + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath}) describe "when the directories along the new path don't exist", -> it "creates the target directory before moving the file", -> @@ -2100,11 +2188,13 @@ describe "TreeView", -> atom.commands.dispatch moveDialog.element, 'core:confirm' waitsFor "tree view to update", -> - root1.find('> .entries > .directory:contains(new)').length > 0 + directories = Array.from(root1.querySelectorAll('.entries .directory')) + directories.filter((f) -> f.textContent is 'new').length > 0 runs -> expect(fs.existsSync(newPath)).toBeTruthy() expect(fs.existsSync(filePath)).toBeFalsy() + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath}) describe "when a file or directory already exists at the target path", -> it "shows an error message and does not close the dialog", -> @@ -2115,21 +2205,22 @@ describe "TreeView", -> atom.commands.dispatch moveDialog.element, 'core:confirm' - expect(moveDialog.errorMessage.text()).toContain 'already exists' - expect(moveDialog).toHaveClass('error') - expect(moveDialog.hasParent()).toBeTruthy() + expect(moveDialog.errorMessage.textContent).toContain 'already exists' + expect(moveDialog.element).toHaveClass('error') + expect(moveDialog.element.parentElement).toBeTruthy() + expect(callback).not.toHaveBeenCalled() describe "when 'core:cancel' is triggered on the move dialog", -> it "removes the dialog and focuses the tree view", -> atom.commands.dispatch moveDialog.element, 'core:cancel' expect(atom.workspace.getModalPanels().length).toBe 0 - expect(treeView.find(".tree-view")).toMatchSelector(':focus') + expect(treeView.element).toHaveFocus() describe "when the move dialog's editor loses focus", -> it "removes the dialog and focuses root view", -> - $(workspaceElement).focus() + workspaceElement.focus() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus() + expect(atom.views.getView(atom.workspace.getCenter().getActivePane())).toHaveFocus() describe "when a file is selected that's name starts with a '.'", -> [dotFilePath, dotFileView, moveDialog] = [] @@ -2137,25 +2228,84 @@ describe "TreeView", -> beforeEach -> dotFilePath = path.join(dirPath, ".dotfile") fs.writeFileSync(dotFilePath, "dot") - dirView[0].collapse() - dirView[0].expand() - dotFileView = treeView.find('.file:contains(.dotfile)') + dirView.collapse() + dirView.expand() + dotFileView = treeView.entryForPath(dotFilePath) - waitsForFileToOpen -> - dotFileView.click() + waitForWorkspaceOpenEvent -> + dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:move") - moveDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + moveDialog = atom.workspace.getModalPanels()[0].getItem() it "selects the entire file name", -> - expect(moveDialog).toExist() + expect(moveDialog.element).toExist() + expect(moveDialog.miniEditor.getText()).toBe(atom.project.relativize(dotFilePath)) + expect(moveDialog.miniEditor.getSelectedText()).toBe '.dotfile' + + describe "when a file is selected that has multiple extensions", -> + [dotFilePath, dotFileView, moveDialog] = [] + + beforeEach -> + dotFilePath = path.join(dirPath, "test.file.txt") + fs.writeFileSync(dotFilePath, "dot dot") + dirView.collapse() + dirView.expand() + dotFileView = treeView.entryForPath(dotFilePath) + + waitForWorkspaceOpenEvent -> + dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + + runs -> + atom.commands.dispatch(treeView.element, "tree-view:move") + moveDialog = atom.workspace.getModalPanels()[0].getItem() + + it "selects only the part of the filename up to the first extension", -> + expect(moveDialog.element).toExist() expect(moveDialog.miniEditor.getText()).toBe(atom.project.relativize(dotFilePath)) - expect(moveDialog.miniEditor.getModel().getSelectedText()).toBe '.dotfile' + expect(moveDialog.miniEditor.getSelectedText()).toBe 'test' + + describe "when a subdirectory is selected", -> + moveDialog = null + + beforeEach -> + jasmine.attachToDOM(workspaceElement) + + waitForWorkspaceOpenEvent -> + atom.workspace.open(filePath) + + waitsForPromise -> + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.toggleFocus().then -> + atom.commands.dispatch(treeView.element, "tree-view:move") + moveDialog = atom.workspace.getModalPanels()[0].getItem() + + afterEach -> + waits 50 # The move specs cause too many false positives because of their async nature, so wait a little bit before we cleanup + + it "opens a move dialog with the folder's current path populated", -> + extension = path.extname(dirPath) + expect(moveDialog.element).toExist() + expect(moveDialog.promptText.textContent).toBe "Enter the new path for the directory." + expect(moveDialog.miniEditor.getText()).toBe(atom.project.relativize(dirPath)) + expect(moveDialog.miniEditor.element).toHaveFocus() + + describe "when the path is changed and confirmed", -> + it "updates text editor paths accordingly", -> + editor = atom.workspace.getCenter().getActiveTextEditor() + expect(editor.getPath()).toBe(filePath) + + newPath = path.join(rootDirPath, 'renamed-dir') + moveDialog.miniEditor.setText(newPath) + + atom.commands.dispatch moveDialog.element, 'core:confirm' + expect(atom.workspace.getActivePaneItem()).toBe(editor) + expect(editor.getPath()).toBe(filePath.replace('test-dir', 'renamed-dir')) describe "when the project is selected", -> it "doesn't display the move dialog", -> - treeView.roots[0].click() + treeView.roots[0].dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:move") expect(atom.workspace.getModalPanels().length).toBe(0) @@ -2166,12 +2316,12 @@ describe "TreeView", -> beforeEach -> jasmine.attachToDOM(workspaceElement) - waitsForFileToOpen -> - fileView.click() + waitForWorkspaceOpenEvent -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:duplicate") - copyDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + copyDialog = atom.workspace.getModalPanels()[0].getItem() afterEach -> waits 50 # The copy specs cause too many false positives because of their async nature, so wait a little bit before we cleanup @@ -2179,11 +2329,11 @@ describe "TreeView", -> it "opens a copy dialog to duplicate with the file's current path populated", -> extension = path.extname(filePath) fileNameWithoutExtension = path.basename(filePath, extension) - expect(copyDialog).toExist() - expect(copyDialog.promptText.text()).toBe "Enter the new path for the duplicate." + expect(copyDialog.element).toExist() + expect(copyDialog.promptText.textContent).toBe "Enter the new path for the duplicate." expect(copyDialog.miniEditor.getText()).toBe(atom.project.relativize(filePath)) - expect(copyDialog.miniEditor.getModel().getSelectedText()).toBe path.basename(fileNameWithoutExtension) - expect(copyDialog.miniEditor).toHaveFocus() + expect(copyDialog.miniEditor.getSelectedText()).toBe fileNameWithoutExtension + expect(copyDialog.miniEditor.element).toHaveFocus() describe "when the path is changed and confirmed", -> describe "when all the directories along the new path exist", -> @@ -2191,19 +2341,19 @@ describe "TreeView", -> newPath = path.join(rootDirPath, 'duplicated-test-file.txt') copyDialog.miniEditor.setText(path.basename(newPath)) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch copyDialog.element, 'core:confirm' waitsFor "tree view to update", -> - root1.find('> .entries > .file:contains(duplicated-test-file.txt)').length > 0 + treeView.entryForPath(newPath) runs -> expect(fs.existsSync(newPath)).toBeTruthy() expect(fs.existsSync(filePath)).toBeTruthy() expect(atom.workspace.getModalPanels().length).toBe 0 - dirView = $(treeView.roots[0].entries).find('.directory:contains(test-dir)') - dirView[0].expand() - expect($(dirView[0].entries).children().length).toBe 1 + dirView = treeView.roots[0].entries.querySelector('.directory') + dirView.expand() + expect(dirView.entries.children.length).toBe 1 expect(atom.workspace.getActiveTextEditor().getPath()).toBe(newPath) describe "when the directories along the new path don't exist", -> @@ -2211,11 +2361,11 @@ describe "TreeView", -> newPath = path.join(rootDirPath, 'new', 'directory', 'duplicated-test-file.txt') copyDialog.miniEditor.setText(newPath) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch copyDialog.element, 'core:confirm' waitsFor "tree view to update", -> - root1.find('> .entries > .directory:contains(new)').length > 0 + treeView.entryForPath(newPath) waitsFor "new path to exist", -> fs.existsSync(newPath) @@ -2232,22 +2382,22 @@ describe "TreeView", -> atom.commands.dispatch copyDialog.element, 'core:confirm' - expect(copyDialog.errorMessage.text()).toContain 'already exists' - expect(copyDialog).toHaveClass('error') - expect(copyDialog.hasParent()).toBeTruthy() + expect(copyDialog.errorMessage.textContent).toContain 'already exists' + expect(copyDialog.element).toHaveClass('error') + expect(copyDialog.element.parentElement).toBeTruthy() describe "when 'core:cancel' is triggered on the copy dialog", -> it "removes the dialog and focuses the tree view", -> jasmine.attachToDOM(treeView.element) atom.commands.dispatch copyDialog.element, 'core:cancel' expect(atom.workspace.getModalPanels().length).toBe 0 - expect(treeView.find(".tree-view")).toMatchSelector(':focus') + expect(treeView.element).toHaveFocus() describe "when the duplicate dialog's editor loses focus", -> it "removes the dialog and focuses root view", -> workspaceElement.focus() expect(atom.workspace.getModalPanels().length).toBe 0 - expect(atom.views.getView(atom.workspace.getActivePane())).toHaveFocus() + expect(atom.views.getView(atom.workspace.getCenter().getActivePane())).toHaveFocus() describe "when a file is selected that's name starts with a '.'", -> [dotFilePath, dotFileView, copyDialog] = [] @@ -2255,25 +2405,47 @@ describe "TreeView", -> beforeEach -> dotFilePath = path.join(dirPath, ".dotfile") fs.writeFileSync(dotFilePath, "dot") - dirView[0].collapse() - dirView[0].expand() - dotFileView = treeView.find('.file:contains(.dotfile)') + dirView.collapse() + dirView.expand() + dotFileView = treeView.entryForPath(dotFilePath) - waitsForFileToOpen -> - dotFileView.click() + waitForWorkspaceOpenEvent -> + dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, "tree-view:duplicate") - copyDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + copyDialog = atom.workspace.getModalPanels()[0].getItem() it "selects the entire file name", -> - expect(copyDialog).toExist() + expect(copyDialog.element).toExist() + expect(copyDialog.miniEditor.getText()).toBe(atom.project.relativize(dotFilePath)) + expect(copyDialog.miniEditor.getSelectedText()).toBe '.dotfile' + + describe "when a file is selected that has multiple extensions", -> + [dotFilePath, dotFileView, copyDialog] = [] + + beforeEach -> + dotFilePath = path.join(dirPath, "test.file.txt") + fs.writeFileSync(dotFilePath, "dot dot") + dirView.collapse() + dirView.expand() + dotFileView = treeView.entryForPath(dotFilePath) + + waitForWorkspaceOpenEvent -> + dotFileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + + runs -> + atom.commands.dispatch(treeView.element, "tree-view:duplicate") + copyDialog = atom.workspace.getModalPanels()[0].getItem() + + it "selects only the part of the filename up to the first extension", -> + expect(copyDialog.element).toExist() expect(copyDialog.miniEditor.getText()).toBe(atom.project.relativize(dotFilePath)) - expect(copyDialog.miniEditor.getModel().getSelectedText()).toBe '.dotfile' + expect(copyDialog.miniEditor.getSelectedText()).toBe 'test' describe "when the project is selected", -> it "doesn't display the copy dialog", -> - treeView.roots[0].click() + treeView.roots[0].dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:duplicate") expect(atom.workspace.getModalPanels().length).toBe(0) @@ -2285,9 +2457,9 @@ describe "TreeView", -> atom.workspace.open('tree-view.js') runs -> - editorElement = atom.views.getView(atom.workspace.getActivePaneItem()) + editorElement = atom.views.getView(atom.workspace.getCenter().getActivePaneItem()) atom.commands.dispatch(editorElement, "tree-view:duplicate") - copyDialog = $(atom.workspace.getModalPanels()[0].getItem()).view() + copyDialog = atom.workspace.getModalPanels()[0].getItem() it "duplicates the current file", -> expect(copyDialog.miniEditor.getText()).toBe('tree-view.js') @@ -2305,7 +2477,7 @@ describe "TreeView", -> spyOn(atom, 'confirm') jasmine.attachToDOM(workspaceElement) treeView.focus() - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, 'tree-view:remove') args = atom.confirm.mostRecentCall.args[0] @@ -2314,8 +2486,8 @@ describe "TreeView", -> it "shows the native alert dialog", -> spyOn(atom, 'confirm') - waitsForFileToOpen -> - fileView.click() + waitForWorkspaceOpenEvent -> + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> atom.commands.dispatch(treeView.element, 'tree-view:remove') @@ -2323,27 +2495,24 @@ describe "TreeView", -> expect(Object.keys(args.buttons)).toEqual ['Move to Trash', 'Cancel'] it "shows a notification on failure", -> + jasmine.attachToDOM(workspaceElement) atom.notifications.clear() - spyOn(atom, 'confirm') + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() - waitsForFileToOpen -> - fileView.click() + spyOn(shell, 'moveItemToTrash').andReturn(false) + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() - runs -> - repeat = 2 - while (repeat > 0) - atom.commands.dispatch(treeView.element, 'tree-view:remove') - args = atom.confirm.mostRecentCall.args[0] - args.buttons["Move to Trash"]() - --repeat + atom.commands.dispatch(treeView.element, 'tree-view:remove') - notificationsNumber = atom.notifications.getNotifications().length - expect(notificationsNumber).toBe 1 - if notificationsNumber is 1 - notification = atom.notifications.getNotifications()[0] - expect(notification.getMessage()).toContain 'The following file couldn\'t be moved to trash' - expect(notification.getDetail()).toContain 'test-file.txt' + notificationsNumber = atom.notifications.getNotifications().length + expect(notificationsNumber).toBe 1 + if notificationsNumber is 1 + notification = atom.notifications.getNotifications()[0] + expect(notification.getMessage()).toContain 'The following file couldn\'t be moved to the trash' + expect(notification.getDetail()).toContain 'test-file.txt' it "does nothing when no file is selected", -> atom.notifications.clear() @@ -2356,6 +2525,122 @@ describe "TreeView", -> expect(atom.confirm.mostRecentCall).not.toExist expect(atom.notifications.getNotifications().length).toBe 0 + describe "when a directory is removed", -> + it "closes editors with files belonging to the removed folder", -> + jasmine.attachToDOM(workspaceElement) + + waitForWorkspaceOpenEvent -> + atom.workspace.open(filePath2) + + waitForWorkspaceOpenEvent -> + atom.workspace.open(filePath3) + + runs -> + openFilePaths = atom.workspace.getTextEditors().map((e) -> e.getPath()) + expect(openFilePaths).toEqual([filePath2, filePath3]) + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + openFilePaths = (editor.getPath() for editor in atom.workspace.getTextEditors()) + expect(openFilePaths).toEqual([]) + + it "focuses the directory's parent folder", -> + jasmine.attachToDOM(workspaceElement) + + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + expect(root1).toHaveClass('selected') + + describe "when a file is removed", -> + it "closes editors with filepaths belonging to the removed file", -> + jasmine.attachToDOM(workspaceElement) + + waitForWorkspaceOpenEvent -> + atom.workspace.open(filePath2) + + runs -> + openFilePaths = atom.workspace.getTextEditors().map((e) -> e.getPath()) + expect(openFilePaths).toEqual([filePath2]) + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + openFilePaths = (editor.getPath() for editor in atom.workspace.getTextEditors()) + expect(openFilePaths).toEqual([]) + + it "focuses the file's parent folder", -> + jasmine.attachToDOM(workspaceElement) + + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() + + runs -> + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + expect(dirView2).toHaveClass('selected') + + describe "when multiple files and folders are deleted", -> + it "does not error when the selected entries form a parent/child relationship", -> + # If dir1 and dir1/file1 are both selected for deletion, + # and dir1 is deleted first, do not error when attempting to delete dir1/file1 + jasmine.attachToDOM(workspaceElement) + atom.notifications.clear() + + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + dirView.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + expect(atom.notifications.getNotifications().length).toBe 0 + + it "focuses the first selected entry's parent folder", -> + jasmine.attachToDOM(workspaceElement) + + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView2.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + expect(root1).toHaveClass('selected') + + describe "when the entry is deleted before 'Move to Trash' is selected", -> + it "does not error", -> + # If the file is marked for deletion but has already been deleted + # outside of Atom by the time the deletion is confirmed, do not error + jasmine.attachToDOM(workspaceElement) + atom.notifications.clear() + + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + treeView.focus() + + spyOn(atom, 'confirm').andCallFake (dialog) -> + # Remove the directory before confirming the deletion + fs.unlinkSync(filePath) + dialog.buttons["Move to Trash"]() + + atom.commands.dispatch(treeView.element, 'tree-view:remove') + expect(atom.notifications.getNotifications().length).toBe 0 + describe "file system events", -> temporaryFilePath = null @@ -2369,44 +2654,41 @@ describe "TreeView", -> runs -> expect(fs.existsSync(temporaryFilePath)).toBeFalsy() - entriesCountBefore = $(treeView.roots[0].entries).find('.entry').length + entriesCountBefore = treeView.roots[0].querySelectorAll('.entry').length fs.writeFileSync temporaryFilePath, 'hi' waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.entry').length is entriesCountBefore + 1 + treeView.roots[0].querySelectorAll('.entry').length is entriesCountBefore + 1 runs -> - expect($(treeView.roots[0].entries).find('.entry').length).toBe entriesCountBefore + 1 - expect($(treeView.roots[0].entries).find('.file:contains(temporary)')).toExist() + expect(treeView.entryForPath(temporaryFilePath)).toExist() fs.removeSync(temporaryFilePath) waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.entry').length is entriesCountBefore + treeView.roots[0].querySelectorAll('.entry').length is entriesCountBefore describe "project changes", -> beforeEach -> atom.project.setPaths([path1]) - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - root1 = $(treeView.roots[0]) + treeView = atom.workspace.getLeftDock().getActivePaneItem() + root1 = treeView.roots[0] describe "when a root folder is added", -> it "maintains expanded folders", -> - root1.find('.directory:contains(dir1)').click() + root1.querySelector('.directory').dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.project.setPaths([path1, path2]) - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - expect(treeView).toExist() - root1 = $(treeView.roots[0]) - expect(root1.find(".directory:contains(dir1)")).toHaveClass("expanded") + treeView = atom.workspace.getLeftDock().getActivePaneItem() + expect(treeView.element).toExist() + expect(treeView.roots[0].querySelector(".directory")).toHaveClass("expanded") it "maintains collapsed (root) folders", -> - root1.click() + root1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.project.setPaths([path1, path2]) - treeView = $(atom.workspace.getLeftPanels()[0].getItem()).view() - expect(treeView).toExist() - root1 = $(treeView.roots[0]) - expect(root1).toHaveClass("collapsed") + treeView = atom.workspace.getLeftDock().getActivePaneItem() + expect(treeView.element).toExist() + expect(treeView.roots[0]).toHaveClass("collapsed") describe "the hideVcsIgnoredFiles config option", -> describe "when the project's path is the repository's working directory", -> @@ -2424,13 +2706,13 @@ describe "TreeView", -> atom.config.set "tree-view.hideVcsIgnoredFiles", false it "hides git-ignored files if the option is set, but otherwise shows them", -> - expect(treeView.find('.file:contains(ignored.txt)').length).toBe 1 + expect(Array.from(treeView.element.querySelectorAll('.file')).map((f) -> f.textContent)).toEqual(['.gitignore', 'ignored.txt']) atom.config.set("tree-view.hideVcsIgnoredFiles", true) - expect(treeView.find('.file:contains(ignored.txt)').length).toBe 0 + expect(Array.from(treeView.element.querySelectorAll('.file')).map((f) -> f.textContent)).toEqual(['.gitignore']) atom.config.set("tree-view.hideVcsIgnoredFiles", false) - expect(treeView.find('.file:contains(ignored.txt)').length).toBe 1 + expect(Array.from(treeView.element.querySelectorAll('.file')).map((f) -> f.textContent)).toEqual(['.gitignore', 'ignored.txt']) describe "when the project's path is a subfolder of the repository's working directory", -> beforeEach -> @@ -2444,7 +2726,7 @@ describe "TreeView", -> atom.config.set("tree-view.hideVcsIgnoredFiles", true) it "does not hide git ignored files", -> - expect(treeView.find('.file:contains(tree-view.js)').length).toBe 1 + expect(Array.from(treeView.element.querySelectorAll('.file')).map((f) -> f.textContent)).toEqual(['.gitignore', 'tree-view.js', 'tree-view.txt']) describe "the hideIgnoredNames config option", -> beforeEach -> @@ -2459,19 +2741,13 @@ describe "TreeView", -> atom.config.set "tree-view.hideIgnoredNames", false it "hides ignored files if the option is set, but otherwise shows them", -> - expect(treeView.find('.directory .name:contains(.git)').length).toBe 1 - expect(treeView.find('.directory .name:contains(test.js)').length).toBe 1 - expect(treeView.find('.directory .name:contains(test.txt)').length).toBe 1 + expect(Array.from(treeView.roots[0].querySelectorAll('.entry')).map((e) -> e.textContent)).toEqual(['.git', 'test.js', 'test.txt']) atom.config.set("tree-view.hideIgnoredNames", true) - expect(treeView.find('.directory .name:contains(.git)').length).toBe 0 - expect(treeView.find('.directory .name:contains(test.js)').length).toBe 0 - expect(treeView.find('.directory .name:contains(test.txt)').length).toBe 1 + expect(Array.from(treeView.roots[0].querySelectorAll('.entry')).map((e) -> e.textContent)).toEqual(['test.txt']) atom.config.set("core.ignoredNames", []) - expect(treeView.find('.directory .name:contains(.git)').length).toBe 1 - expect(treeView.find('.directory .name:contains(test.js)').length).toBe 1 - expect(treeView.find('.directory .name:contains(test.txt)').length).toBe 1 + expect(Array.from(treeView.roots[0].querySelectorAll('.entry')).map((e) -> e.textContent)).toEqual(['.git', 'test.js', 'test.txt']) describe "the squashedDirectoryName config option", -> beforeEach -> @@ -2493,6 +2769,14 @@ describe "TreeView", -> iotaDirPath = path.join(lambdaDirPath, "iota") kappaDirPath = path.join(lambdaDirPath, "kappa") + muDirPath = path.join(rootDirPath, "mu") + nuDirPath = path.join(muDirPath, "nu") + xiDirPath1 = path.join(muDirPath, "xi") + xiDirPath2 = path.join(nuDirPath, "xi") + + omicronDirPath = path.join(rootDirPath, "omicron") + piDirPath = path.join(omicronDirPath, "pi") + fs.makeTreeSync(zetaDirPath) fs.writeFileSync(zetaFilePath, "doesn't matter") @@ -2509,6 +2793,14 @@ describe "TreeView", -> fs.makeTreeSync(iotaDirPath) fs.makeTreeSync(kappaDirPath) + fs.makeTreeSync(muDirPath) + fs.makeTreeSync(nuDirPath) + fs.makeTreeSync(xiDirPath1) + fs.makeTreeSync(xiDirPath2) + + fs.makeTreeSync(omicronDirPath) + fs.makeTreeSync(piDirPath) + atom.project.setPaths([rootDirPath]) it "defaults to disabled", -> @@ -2518,38 +2810,113 @@ describe "TreeView", -> beforeEach -> atom.config.set('tree-view.squashDirectoryNames', true) + it "does not squash root directories", -> + rootDir = fs.absolute(temp.mkdirSync('tree-view')) + zetaDir = path.join(rootDir, "zeta") + fs.makeTreeSync(zetaDir) + atom.project.setPaths([rootDir]) + jasmine.attachToDOM(workspaceElement) + + rootDirPath = treeView.roots[0].getPath() + expect(rootDirPath).toBe(rootDir) + zetaDirPath = findDirectoryContainingText(treeView.roots[0], 'zeta').getPath() + expect(zetaDirPath).toBe(zetaDir) + it "does not squash a file in to a DirectoryViews", -> - zetaDir = $(treeView.roots[0].entries).find('.directory:contains(zeta):first') - zetaDir[0].expand() - zetaEntries = [].slice.call(zetaDir[0].children[1].children).map (element) -> + zetaDir = findDirectoryContainingText(treeView.roots[0], 'zeta') + zetaDir.expand() + zetaEntries = [].slice.call(zetaDir.children[1].children).map (element) -> element.innerText expect(zetaEntries).toEqual(["zeta.txt"]) it "squashes two dir names when the first only contains a single dir", -> - betaDir = $(treeView.roots[0].entries).find(".directory:contains(alpha#{path.sep}beta):first") - betaDir[0].expand() - betaEntries = [].slice.call(betaDir[0].children[1].children).map (element) -> + betaDir = findDirectoryContainingText(treeView.roots[0], "alpha#{path.sep}beta") + betaDir.expand() + betaEntries = [].slice.call(betaDir.children[1].children).map (element) -> element.innerText expect(betaEntries).toEqual(["beta.txt"]) it "squashes three dir names when the first and second only contain single dirs", -> - epsilonDir = $(treeView.roots[0].entries).find(".directory:contains(gamma#{path.sep}delta#{path.sep}epsilon):first") - epsilonDir[0].expand() - epsilonEntries = [].slice.call(epsilonDir[0].children[1].children).map (element) -> + epsilonDir = findDirectoryContainingText(treeView.roots[0], "gamma#{path.sep}delta#{path.sep}epsilon") + epsilonDir.expand() + epsilonEntries = [].slice.call(epsilonDir.children[1].children).map (element) -> element.innerText expect(epsilonEntries).toEqual(["theta.txt"]) it "does not squash a dir name when there are two child dirs ", -> - lambdaDir = $(treeView.roots[0].entries).find('.directory:contains(lambda):first') - lambdaDir[0].expand() - lambdaEntries = [].slice.call(lambdaDir[0].children[1].children).map (element) -> + lambdaDir = findDirectoryContainingText(treeView.roots[0], "lambda") + lambdaDir.expand() + lambdaEntries = [].slice.call(lambdaDir.children[1].children).map (element) -> element.innerText expect(lambdaEntries).toEqual(["iota", "kappa"]) + describe "when a squashed directory is deleted", -> + it "un-squashes the directories", -> + jasmine.attachToDOM(workspaceElement) + piDir = findDirectoryContainingText(treeView.roots[0], "omicron#{path.sep}pi") + treeView.focus() + treeView.selectEntry(piDir) + spyOn(atom, 'confirm').andCallFake (dialog) -> + dialog.buttons["Move to Trash"]() + atom.commands.dispatch(treeView.element, 'tree-view:remove') + + omicronDir = findDirectoryContainingText(treeView.roots[0], "omicron") + expect(omicronDir.header.textContent).toEqual("omicron") + + describe "when a file is created within a directory with another squashed directory", -> + it "un-squashes the directories", -> + jasmine.attachToDOM(workspaceElement) + piDir = findDirectoryContainingText(treeView.roots[0], "omicron#{path.sep}pi") + expect(piDir).not.toBeNull() + # omicron is a squashed dir, so searching for omicron would give us omicron/pi instead + omicronPath = piDir.getPath().replace "#{path.sep}pi", "" + sigmaFilePath = path.join(omicronPath, "sigma.txt") + fs.writeFileSync(sigmaFilePath, "doesn't matter") + treeView.updateRoots() + + omicronDir = findDirectoryContainingText(treeView.roots[0], "omicron") + expect(omicronDir.header.textContent).toEqual("omicron") + omicronDir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + piDir = findDirectoryContainingText(omicronDir, "pi") + expect(piDir.header.textContent).toEqual("pi") + sigmaFile = findFileContainingText(omicronDir, "sigma.txt") + expect(sigmaFile.fileName.textContent).toEqual("sigma.txt") + + describe "when a directory is created within a directory with another squashed directory", -> + it "un-squashes the directories", -> + jasmine.attachToDOM(workspaceElement) + piDir = findDirectoryContainingText(treeView.roots[0], "omicron#{path.sep}pi") + expect(piDir).not.toBeNull() + # omicron is a squashed dir, so searching for omicron would give us omicron/pi instead + omicronPath = piDir.getPath().replace "#{path.sep}pi", "" + rhoDirPath = path.join(omicronPath, "rho") + fs.makeTreeSync(rhoDirPath) + treeView.updateRoots() + + omicronDir = findDirectoryContainingText(treeView.roots[0], "omicron") + expect(omicronDir.header.textContent).toEqual("omicron") + omicronDir.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + piDir = findDirectoryContainingText(omicronDir, "pi") + expect(piDir.header.textContent).toEqual("pi") + rhoDir = findDirectoryContainingText(omicronDir, "rho") + expect(rhoDir.header.textContent).toEqual("rho") + + describe "when a directory is reloaded", -> + it "squashes the directory names the last of which is same as an unsquashed directory", -> + muDir = findDirectoryContainingText(treeView.roots[0], "mu") + muDir.expand() + muEntries = Array.from(muDir.children[1].children).map (element) -> element.innerText + expect(muEntries).toEqual(["nu#{path.sep}xi", "xi"]) + + muDir.expand() + muDir.reload() + muEntries = Array.from(muDir.children[1].children).map (element) -> element.innerText + expect(muEntries).toEqual(["nu#{path.sep}xi", "xi"]) + describe "Git status decorations", -> [projectPath, modifiedFile, originalFileContent] = [] @@ -2579,60 +2946,64 @@ describe "TreeView", -> treeView.useSyncFS = true treeView.updateRoots() - $(treeView.roots[0].entries).find('.directory:contains(dir)')[0].expand() + treeView.roots[0].entries.querySelectorAll('.directory')[1].expand() describe "when the project is the repository root", -> it "adds a custom style", -> - expect(treeView.find('.icon-repo').length).toBe 1 + expect(treeView.element.querySelectorAll('.icon-repo').length).toBe 1 describe "when a file is modified", -> it "adds a custom style", -> - $(treeView.roots[0].entries).find('.directory:contains(dir)')[0].expand() - expect(treeView.find('.file:contains(b.txt)')).toHaveClass 'status-modified' + expect(treeView.element.querySelector('.file.status-modified')).toHaveText('b.txt') - describe "when a directory if modified", -> + describe "when a directory is modified", -> it "adds a custom style", -> - expect(treeView.find('.directory:contains(dir)')).toHaveClass 'status-modified' + expect(treeView.element.querySelector('.directory.status-modified').header).toHaveText('dir') describe "when a file is new", -> it "adds a custom style", -> - $(treeView.roots[0].entries).find('.directory:contains(dir2)')[0].expand() - expect(treeView.find('.file:contains(new2)')).toHaveClass 'status-added' + treeView.roots[0].entries.querySelectorAll('.directory')[2].expand() + expect(treeView.element.querySelector('.file.status-added')).toHaveText('new2') describe "when a directory is new", -> it "adds a custom style", -> - expect(treeView.find('.directory:contains(dir2)')).toHaveClass 'status-added' + expect(treeView.element.querySelector('.directory.status-added').header).toHaveText('dir2') describe "when a file is ignored", -> it "adds a custom style", -> - expect(treeView.find('.file:contains(ignored.txt)')).toHaveClass 'status-ignored' + expect(treeView.element.querySelector('.file.status-ignored')).toHaveText('ignored.txt') describe "when a file is selected in a directory", -> beforeEach -> jasmine.attachToDOM(workspaceElement) treeView.focus() - element.expand() for element in treeView.find('.directory') - fileView = treeView.find('.file:contains(new2)') + element.expand() for element in treeView.element.querySelectorAll('.directory') + fileView = treeView.element.querySelector('.file.status-added') expect(fileView).not.toBeNull() - fileView.click() + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) describe "when the file is deleted", -> it "updates the style of the directory", -> + callback = jasmine.createSpy("onEntryDeleted") + treeView.onEntryDeleted(callback) + + deletedPath = treeView.selectedEntry().getPath() expect(treeView.selectedEntry().getPath()).toContain(path.join('dir2', 'new2')) - dirView = $(treeView.roots[0].entries).find('.directory:contains(dir2)') + dirView = findDirectoryContainingText(treeView.roots[0], 'dir2') expect(dirView).not.toBeNull() - spyOn(dirView[0].directory, 'updateStatus') + spyOn(dirView.directory, 'updateStatus') spyOn(atom, 'confirm').andCallFake (dialog) -> dialog.buttons["Move to Trash"]() atom.commands.dispatch(treeView.element, 'tree-view:remove') - expect(dirView[0].directory.updateStatus).toHaveBeenCalled() + expect(dirView.directory.updateStatus).toHaveBeenCalled() + expect(callback).toHaveBeenCalledWith({path: deletedPath}) - describe "when the project is a symbolic link to the repository root", -> + describe "on #darwin, when the project is a symbolic link to the repository root", -> beforeEach -> symlinkPath = temp.path('tree-view-project') - fs.symlinkSync(projectPath, symlinkPath) + fs.symlinkSync(projectPath, symlinkPath, 'junction') atom.project.setPaths([symlinkPath]) - $(treeView.roots[0].entries).find('.directory:contains(dir)')[0].expand() + treeView.roots[0].entries.querySelectorAll('.directory')[1].expand() waitsFor (done) -> disposable = atom.project.getRepositories()[0].onDidChangeStatuses -> @@ -2641,77 +3012,19 @@ describe "TreeView", -> describe "when a file is modified", -> it "updates its and its parent directories' styles", -> - expect(treeView.find('.file:contains(b.txt)')).toHaveClass 'status-modified' - expect(treeView.find('.directory:contains(dir)')).toHaveClass 'status-modified' + expect(treeView.element.querySelector('.file.status-modified')).toHaveText('b.txt') + expect(treeView.element.querySelector('.directory.status-modified').header).toHaveText('dir') describe "when a file loses its modified status", -> it "updates its and its parent directories' styles", -> fs.writeFileSync(modifiedFile, originalFileContent) atom.project.getRepositories()[0].getPathStatus(modifiedFile) - expect(treeView.find('.file:contains(b.txt)')).not.toHaveClass 'status-modified' - expect(treeView.find('.directory:contains(dir)')).not.toHaveClass 'status-modified' - - describe "when the resize handle is double clicked", -> - beforeEach -> - treeView.width(10).find('.list-tree').width 100 - - it "sets the width of the tree to be the width of the list", -> - expect(treeView.width()).toBe 10 - treeView.find('.tree-view-resize-handle').trigger 'dblclick' - expect(treeView.width()).toBeGreaterThan 10 - - treeView.width(1000) - treeView.find('.tree-view-resize-handle').trigger 'dblclick' - expect(treeView.width()).toBeLessThan 1000 - - describe "when other panels are added", -> - beforeEach -> - jasmine.attachToDOM(workspaceElement) - - it "should resize normally", -> - expect(treeView).toBeVisible() - expect(atom.workspace.getLeftPanels().length).toBe(1) - - treeView.width(100) - - expect(treeView.width()).toBe(100) - - panel = document.createElement('div') - panel.style.width = '100px' - atom.workspace.addLeftPanel({item: panel, priority: 10}) - - expect(atom.workspace.getLeftPanels().length).toBe(2) - expect(treeView.width()).toBe(100) - - treeView.resizeTreeView({pageX: 250, which: 1}) - - expect(treeView.width()).toBe(150) - - it "should resize normally on the right side", -> - atom.commands.dispatch(workspaceElement, 'tree-view:toggle-side') - expect(treeView).toMatchSelector('[data-show-on-right-side="true"]') - - expect(treeView).toBeVisible() - expect(atom.workspace.getRightPanels().length).toBe(1) - - treeView.width(100) - - expect(treeView.width()).toBe(100) - - panel = document.createElement('div') - panel.style.width = '100px' - atom.workspace.addRightPanel({item: panel, priority: 10}) - - expect(atom.workspace.getRightPanels().length).toBe(2) - expect(treeView.width()).toBe(100) - - treeView.resizeTreeView({pageX: $(document.body).width() - 250, which: 1}) - - expect(treeView.width()).toBe(150) + expect(treeView.element.querySelector('.file.status-modified')).not.toExist() + expect(treeView.element.querySelector('.directory.status-modified')).not.toExist() describe "selecting items", -> - [dirView, fileView1, fileView2, fileView3, treeView, rootDirPath, dirPath, filePath1, filePath2, filePath3] = [] + [dirView, fileView1, fileView2, fileView3, fileView4, fileView5, treeView, rootDirPath, dirPath, filePath1, filePath2, filePath3, filePath4, filePath5] = [] beforeEach -> rootDirPath = fs.absolute(temp.mkdirSync('tree-view')) @@ -2720,49 +3033,82 @@ describe "TreeView", -> filePath1 = path.join(dirPath, "test-file1.txt") filePath2 = path.join(dirPath, "test-file2.txt") filePath3 = path.join(dirPath, "test-file3.txt") + filePath4 = path.join(dirPath, "test-file4.txt") + filePath5 = path.join(dirPath, "test-file5.txt") fs.makeTreeSync(dirPath) fs.writeFileSync(filePath1, "doesn't matter") fs.writeFileSync(filePath2, "doesn't matter") fs.writeFileSync(filePath3, "doesn't matter") + fs.writeFileSync(filePath4, "doesn't matter") + fs.writeFileSync(filePath5, "doesn't matter") atom.project.setPaths([rootDirPath]) - dirView = $(treeView.roots[0].entries).find('.directory:contains(test-dir)') - dirView[0].expand() - fileView1 = treeView.find('.file:contains(test-file1.txt)') - fileView2 = treeView.find('.file:contains(test-file2.txt)') - fileView3 = treeView.find('.file:contains(test-file3.txt)') - - describe 'selecting multiple items', -> - it 'switches the contextual menu to muli-select mode', -> - fileView1.click() - fileView2.trigger($.Event('mousedown', {shiftKey: true})) - expect(treeView.find('.tree-view')).toHaveClass('multi-select') - fileView3.trigger($.Event('mousedown')) - expect(treeView.find('.tree-view')).toHaveClass('full-menu') + dirView = treeView.entryForPath(dirPath) + dirView.expand() + [fileView1, fileView2, fileView3, fileView4, fileView5] = dirView.querySelectorAll('.file') describe 'selecting multiple items', -> it 'switches the contextual menu to muli-select mode', -> - fileView1.click() - fileView2.trigger($.Event('mousedown', {shiftKey: true})) - expect(treeView.find('.tree-view')).toHaveClass('multi-select') + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView2.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, shiftKey: true})) + expect(treeView.list).toHaveClass('multi-select') + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + expect(treeView.list).toHaveClass('full-menu') + + describe 'selecting one of the selected items', -> + it 'maintains multi-select for dragging', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView2.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, shiftKey: true})) + fileView1.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + expect(treeView.list).not.toHaveClass('full-menu') + expect(treeView.list).toHaveClass('multi-select') + + it 'switches to full-menu on mouseup', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView2.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, shiftKey: true})) + fileView1.dispatchEvent(new MouseEvent('mousedown', {bubbles: true})) + fileView1.dispatchEvent(new MouseEvent('mouseup', {bubbles: true})) + expect(treeView.list).toHaveClass('full-menu') + expect(treeView.list).not.toHaveClass('multi-select') describe 'using the shift key', -> it 'selects the items between the already selected item and the shift clicked item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {shiftKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, shiftKey: true})) expect(fileView1).toHaveClass('selected') expect(fileView2).toHaveClass('selected') expect(fileView3).toHaveClass('selected') describe 'using the metakey(cmd) key', -> - it 'selects the cmd clicked item in addition to the original selected item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {metaKey: true})) + it 'selects the cmd-clicked item in addition to the original selected item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) expect(fileView1).toHaveClass('selected') + expect(fileView2).not.toHaveClass('selected') expect(fileView3).toHaveClass('selected') + + describe 'using the metakey(cmd) key on already selected item', -> + it 'deselects just the cmd-clicked item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView1.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView1.dispatchEvent(new MouseEvent('mouseup', {bubbles: true, metaKey: true})) + expect(fileView1).not.toHaveClass('selected') expect(fileView2).not.toHaveClass('selected') + expect(fileView3).toHaveClass('selected') + + describe 'using the shift and metakey(cmd) keys', -> + it 'selects the items between the last cmd-clicked item and the clicked item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView5.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true, shiftKey: true})) + expect(fileView1).toHaveClass('selected') + expect(fileView2).not.toHaveClass('selected') + expect(fileView3).toHaveClass('selected') + expect(fileView4).toHaveClass('selected') + expect(fileView5).toHaveClass('selected') describe 'non-darwin platform', -> originalPlatform = process.platform @@ -2772,13 +3118,13 @@ describe "TreeView", -> Object.defineProperty(process, "platform", {__proto__: null, value: 'win32'}) afterEach -> - # Ensure that process.platform is set back to it's original value + # Ensure that process.platform is set back to its original value Object.defineProperty(process, "platform", {__proto__: null, value: originalPlatform}) describe 'using the ctrl key', -> - it 'selects the ctrl clicked item in addition to the original selected item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + it 'selects the ctrl-clicked item in addition to the original selected item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(fileView1).toHaveClass('selected') expect(fileView3).toHaveClass('selected') expect(fileView2).not.toHaveClass('selected') @@ -2791,62 +3137,62 @@ describe "TreeView", -> Object.defineProperty(process, "platform", {__proto__: null, value: 'darwin'}) afterEach -> - # Ensure that process.platform is set back to it's original value + # Ensure that process.platform is set back to its original value Object.defineProperty(process, "platform", {__proto__: null, value: originalPlatform}) describe 'using the ctrl key', -> - describe "previous item is selected but the ctrl clicked item is not", -> + describe "previous item is selected but the ctrl-clicked item is not", -> it 'selects the clicked item, but deselects the previous item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(fileView1).not.toHaveClass('selected') expect(fileView3).toHaveClass('selected') expect(fileView2).not.toHaveClass('selected') it 'displays the full contextual menu', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(treeView.list).toHaveClass('full-menu') expect(treeView.list).not.toHaveClass('multi-select') - describe 'previous item is selected including the ctrl clicked', -> + describe 'previous item is selected including the ctrl-clicked', -> it 'displays the multi-select menu', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {metaKey: true})) - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(treeView.list).not.toHaveClass('full-menu') expect(treeView.list).toHaveClass('multi-select') it 'does not deselect any of the items', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {metaKey: true})) - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(fileView1).toHaveClass('selected') expect(fileView3).toHaveClass('selected') describe 'when clicked item is the only item selected', -> it 'displays the full contextual menu', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(treeView.list).toHaveClass('full-menu') expect(treeView.list).not.toHaveClass('multi-select') describe 'when no item is selected', -> - it 'selects the ctrl clicked item', -> - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + it 'selects the ctrl-clicked item', -> + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(fileView3).toHaveClass('selected') it 'displays the full context menu', -> - fileView3.trigger($.Event('mousedown', {ctrlKey: true})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, ctrlKey: true})) expect(treeView.list).toHaveClass('full-menu') expect(treeView.list).not.toHaveClass('multi-select') describe "right-clicking", -> describe 'when multiple items are selected', -> it 'displays the multi-select context menu', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {metaKey: true})) - fileView3.trigger($.Event('mousedown', {button: 2})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, metaKey: true})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(fileView1).toHaveClass('selected') expect(fileView3).toHaveClass('selected') expect(treeView.list).not.toHaveClass('full-menu') @@ -2854,28 +3200,28 @@ describe "TreeView", -> describe 'when a single item is selected', -> it 'displays the full context menu', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {button: 2})) + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(treeView.list).toHaveClass('full-menu') expect(treeView.list).not.toHaveClass('multi-select') - it 'selects right clicked item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {button: 2})) + it 'selects right-clicked item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(fileView3).toHaveClass('selected') - it 'de-selects the previously selected item', -> - fileView1.click() - fileView3.trigger($.Event('mousedown', {button: 2})) + it 'deselects the previously selected item', -> + fileView1.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(fileView1).not.toHaveClass('selected') describe 'when no item is selected', -> - it 'selects the right clicked item', -> - fileView3.trigger($.Event('mousedown', {button: 2})) + it 'selects the right-clicked item', -> + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(fileView3).toHaveClass('selected') it 'shows the full context menu', -> - fileView3.trigger($.Event('mousedown', {button: 2})) + fileView3.dispatchEvent(new MouseEvent('mousedown', {bubbles: true, button: 2})) expect(fileView3).toHaveClass('selected') expect(treeView.list).toHaveClass('full-menu') expect(treeView.list).not.toHaveClass('multi-select') @@ -2924,16 +3270,16 @@ describe "TreeView", -> expect(topLevelEntries).toEqual(["alpha", "gamma", "alpha.txt", "zeta.txt"]) - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() - alphaEntries = [].slice.call(alphaDir[0].children[1].children).map (element) -> + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + alphaEntries = [].slice.call(alphaDir.children[1].children).map (element) -> element.innerText expect(alphaEntries).toEqual(["eta", "beta.txt"]) - gammaDir = $(treeView.roots[0].entries).find('.directory:contains(gamma):first') - gammaDir[0].expand() - gammaEntries = [].slice.call(gammaDir[0].children[1].children).map (element) -> + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + gammaEntries = [].slice.call(gammaDir.children[1].children).map (element) -> element.innerText expect(gammaEntries).toEqual(["theta", "delta.txt", "epsilon.txt"]) @@ -2946,16 +3292,16 @@ describe "TreeView", -> expect(topLevelEntries).toEqual(["alpha", "alpha.txt", "gamma", "zeta.txt"]) - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() - alphaEntries = [].slice.call(alphaDir[0].children[1].children).map (element) -> + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + alphaEntries = [].slice.call(alphaDir.children[1].children).map (element) -> element.innerText expect(alphaEntries).toEqual(["beta.txt", "eta"]) - gammaDir = $(treeView.roots[0].entries).find('.directory:contains(gamma):first') - gammaDir[0].expand() - gammaEntries = [].slice.call(gammaDir[0].children[1].children).map (element) -> + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + gammaEntries = [].slice.call(gammaDir.children[1].children).map (element) -> element.innerText expect(gammaEntries).toEqual(["delta.txt", "epsilon.txt", "theta"]) @@ -2994,8 +3340,8 @@ describe "TreeView", -> it "handle errors thrown when spawning the OS file manager", -> spyOn(treeView, 'fileManagerCommandForPath').andReturn - command: '/this/command/does/not/exist' - label: 'Finder' + command: path.normalize('/this/command/does/not/exist') + label: 'OS file manager' args: ['foo'] treeView.showSelectedEntryInFileManager() @@ -3004,8 +3350,33 @@ describe "TreeView", -> atom.notifications.getNotifications().length is 1 runs -> - expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Opening folder in Finder failed' - expect(atom.notifications.getNotifications()[0].getDetail()).toContain 'ENOENT' + expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Opening folder in OS file manager failed' + expect(atom.notifications.getNotifications()[0].getDetail()).toContain if process.platform is 'win32' then 'cannot find the path' else 'ENOENT' + + describe "showCurrentFileInFileManager()", -> + it "does nothing when no file is opened", -> + expect(atom.workspace.getCenter().getPaneItems().length).toBe(0) + expect(treeView.showCurrentFileInFileManager()).toBeUndefined() + + it "does nothing when only an untitled tab is opened", -> + waitsForPromise -> + atom.workspace.open() + runs -> + workspaceElement.focus() + expect(treeView.showCurrentFileInFileManager()).toBeUndefined() + + it "shows file in file manager when some file is opened", -> + filePath = path.join(os.tmpdir(), 'non-project-file.txt') + fs.writeFileSync(filePath, 'test') + waitsForPromise -> + atom.workspace.open(filePath) + + runs -> + {BufferedProcess} = require 'atom' + spyOn(BufferedProcess.prototype, 'spawn').andCallFake -> + fileManagerProcess = treeView.showCurrentFileInFileManager() + expect(fileManagerProcess instanceof BufferedProcess).toBeTruthy() + fileManagerProcess.kill() describe "when reloading a directory with deletions and additions", -> it "does not throw an error (regression)", -> @@ -3017,29 +3388,28 @@ describe "TreeView", -> treeView.roots[0].expand() expect(treeView.roots[0].directory.serializeExpansionState()).toEqual isExpanded: true - entries: - entries: - isExpanded: false - entries: {} + entries: new Map().set('entries', + isExpanded: false + entries: new Map()) fs.removeSync(entriesPath) treeView.roots[0].reload() expect(treeView.roots[0].directory.serializeExpansionState()).toEqual isExpanded: true - entries: {} + entries: new Map() fs.mkdirSync(path.join(projectPath, 'other')) treeView.roots[0].reload() expect(treeView.roots[0].directory.serializeExpansionState()).toEqual isExpanded: true - entries: - other: - isExpanded: false - entries: {} + entries: new Map().set('other', + isExpanded: false + entries: new Map()) describe "Dragging and dropping files", -> deltaFilePath = null gammaDirPath = null + thetaFilePath = null beforeEach -> rootDirPath = fs.absolute(temp.mkdirSync('tree-view')) @@ -3055,6 +3425,7 @@ describe "TreeView", -> deltaFilePath = path.join(gammaDirPath, "delta.txt") epsilonFilePath = path.join(gammaDirPath, "epsilon.txt") thetaDirPath = path.join(gammaDirPath, "theta") + thetaFilePath = path.join(thetaDirPath, "theta.txt") fs.writeFileSync(alphaFilePath, "doesn't matter") fs.writeFileSync(zetaFilePath, "doesn't matter") @@ -3067,21 +3438,24 @@ describe "TreeView", -> fs.writeFileSync(deltaFilePath, "doesn't matter") fs.writeFileSync(epsilonFilePath, "doesn't matter") fs.makeTreeSync(thetaDirPath) + fs.writeFileSync(thetaFilePath, "doesn't matter") atom.project.setPaths([rootDirPath]) describe "when dragging a FileView onto a DirectoryView's header", -> it "should add the selected class to the DirectoryView", -> # Dragging theta onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() - gammaDir = $(treeView.roots[0].entries).find('.directory:contains(gamma):first') - gammaDir[0].expand() - deltaFile = gammaDir[0].entries.children[2] + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + deltaFile = gammaDir.entries.children[2] [dragStartEvent, dragEnterEvent, dropEvent] = - eventHelpers.buildInternalDragEvents(deltaFile, alphaDir.find('.header')[0]) + eventHelpers.buildInternalDragEvents([deltaFile], alphaDir.querySelector('.header')) treeView.onDragStart(dragStartEvent) + expect(deltaFile).toHaveClass('selected') treeView.onDragEnter(dragEnterEvent) expect(alphaDir).toHaveClass('selected') @@ -3096,104 +3470,184 @@ describe "TreeView", -> describe "when dropping a FileView onto a DirectoryView's header", -> it "should move the file to the hovered directory", -> # Dragging delta.txt onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + deltaFile = gammaDir.entries.children[2] + + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents([deltaFile], alphaDir.querySelector('.header'), alphaDir) + + runs -> + treeView.onDragStart(dragStartEvent) + treeView.onDrop(dropEvent) + expect(alphaDir.children.length).toBe 2 + + waitsFor "directory view contents to refresh", -> + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + + runs -> + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + + describe "when dropping multiple FileViews onto a DirectoryView's header", -> + it "should move the files to the hovered directory", -> + # Dragging delta.txt onto alphaDir + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + gammaFiles = [].slice.call(gammaDir.entries.children, 1, 3) + + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents(gammaFiles, alphaDir.querySelector('.header'), alphaDir) - gammaDir = $(treeView.roots[0].entries).find('.directory:contains(gamma):first') - gammaDir[0].expand() - deltaFile = gammaDir[0].entries.children[2] + runs -> + treeView.onDragStart(dragStartEvent) + treeView.onDrop(dropEvent) + expect(alphaDir.entries.children.length).toBe 2 + + waitsFor "directory view contents to refresh", -> + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + + runs -> + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 4 + + describe "when dropping a DirectoryView and FileViews onto a DirectoryView's header", -> + it "should move the files and directory to the hovered directory", -> + # Dragging alpha.txt and alphaDir into thetaDir + alphaFile = treeView.roots[0].entries.children[2] + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + thetaDir = findDirectoryContainingText(treeView.roots[0], 'theta') + thetaDir.expand() + + dragged = [alphaFile, alphaDir] [dragStartEvent, dragEnterEvent, dropEvent] = - eventHelpers.buildInternalDragEvents(deltaFile, alphaDir.find('.header')[0], alphaDir[0]) + eventHelpers.buildInternalDragEvents(dragged, thetaDir.querySelector('.header'), thetaDir) runs -> treeView.onDragStart(dragStartEvent) treeView.onDrop(dropEvent) - expect(alphaDir[0].children.length).toBe 2 + expect(thetaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'theta').querySelectorAll('.entry').length > 2 runs -> - expect($(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length).toBe 3 + thetaDir.expand() + expect(thetaDir.querySelectorAll('.entry').length).toBe 3 + # alpha dir still has all its entries + alphaDir = findDirectoryContainingText(thetaDir.entries, 'alpha') + alphaDir.expand() + expect(alphaDir.querySelectorAll('.entry').length).toBe 2 describe "when dropping a DirectoryView onto a DirectoryView's header", -> it "should move the directory to the hovered directory", -> # Dragging thetaDir onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() - gammaDir = $(treeView.roots[0].entries).find('.directory:contains(gamma):first') - gammaDir[0].expand() - thetaDir = gammaDir[0].entries.children[0] + gammaDir = findDirectoryContainingText(treeView.roots[0], 'gamma') + gammaDir.expand() + thetaDir = gammaDir.entries.children[0] + thetaDir.expand() - [dragStartEvent, dragEnterEvent, dropEvent] = - eventHelpers.buildInternalDragEvents(thetaDir, alphaDir.find('.header')[0], alphaDir[0]) + waitForWorkspaceOpenEvent -> + atom.workspace.open(thetaFilePath) runs -> + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents([thetaDir], alphaDir.querySelector('.header'), alphaDir) treeView.onDragStart(dragStartEvent) treeView.onDrop(dropEvent) - expect(alphaDir[0].children.length).toBe 2 + expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 runs -> - expect($(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + editor = atom.workspace.getActiveTextEditor() + expect(editor.getPath()).toBe(thetaFilePath.replace('gamma', 'alpha')) + + describe "when dropping a DirectoryView and FileViews onto the same DirectoryView's header", -> + it "should not move the files and directory to the hovered directory", -> + # Dragging alpha.txt and alphaDir into alphaDir + alphaFile = treeView.roots[0].entries.children[2] + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + + dragged = [alphaFile, alphaDir] + + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents(dragged, alphaDir.querySelector('.header'), alphaDir) + + spyOn(treeView, 'moveEntry') + + treeView.onDragStart(dragStartEvent) + treeView.onDrop(dropEvent) + expect(treeView.moveEntry).not.toHaveBeenCalled() describe "when dragging a file from the OS onto a DirectoryView's header", -> it "should move the file to the hovered directory", -> # Dragging delta.txt from OS file explorer onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() - dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath], alphaDir[0]) + dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath], alphaDir) runs -> treeView.onDrop(dropEvent) - expect(alphaDir[0].children.length).toBe 2 + expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 runs -> - expect($(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 describe "when dragging a directory from the OS onto a DirectoryView's header", -> it "should move the directory to the hovered directory", -> # Dragging gammaDir from OS file explorer onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() - dropEvent = eventHelpers.buildExternalDropEvent([gammaDirPath], alphaDir[0]) + dropEvent = eventHelpers.buildExternalDropEvent([gammaDirPath], alphaDir) runs -> treeView.onDrop(dropEvent) - expect(alphaDir[0].children.length).toBe 2 + expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 runs -> - expect($(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 describe "when dragging a file and directory from the OS onto a DirectoryView's header", -> it "should move the file and directory to the hovered directory", -> # Dragging delta.txt and gammaDir from OS file explorer onto alphaDir - alphaDir = $(treeView.roots[0].entries).find('.directory:contains(alpha):first') - alphaDir[0].expand() + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() - dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath, gammaDirPath], alphaDir[0]) + dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath, gammaDirPath], alphaDir) runs -> treeView.onDrop(dropEvent) - expect(alphaDir[0].children.length).toBe 2 + expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - $(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length > 3 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 3 runs -> - expect($(treeView.roots[0].entries).find('.directory:contains(alpha):first .entry').length).toBe 4 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 4 describe "the alwaysOpenExisting config option", -> it "defaults to unset", -> @@ -3207,52 +3661,52 @@ describe "TreeView", -> it "selects the files and opens it in the active editor, without changing focus", -> treeView.focus() - waitsForFileToOpen -> - sampleJs.trigger clickEvent(originalEvent: {detail: 1}) + waitForWorkspaceOpenEvent -> + sampleJs.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> expect(sampleJs).toHaveClass 'selected' - expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') - expect(treeView.list).toHaveFocus() + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.js') + expect(treeView.element).toHaveFocus() - waitsForFileToOpen -> - sampleTxt.trigger clickEvent(originalEvent: {detail: 1}) + waitForWorkspaceOpenEvent -> + sampleTxt.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> expect(sampleTxt).toHaveClass 'selected' - expect(treeView.find('.selected').length).toBe 1 - expect(atom.workspace.getActivePaneItem().getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') - expect(treeView.list).toHaveFocus() + expect(treeView.element.querySelectorAll('.selected').length).toBe 1 + expect(atom.workspace.getCenter().getActivePaneItem().getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') + expect(treeView.element).toHaveFocus() describe "opening existing opened files in existing split panes", -> beforeEach -> jasmine.attachToDOM(workspaceElement) - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> selectEntry 'tree-view.js' atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right') - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> selectEntry 'tree-view.txt' atom.commands.dispatch(treeView.element, 'tree-view:open-selected-entry-right') it "should have opened both panes", -> - expect(atom.workspace.getPanes().length).toBe 2 + expect(atom.workspace.getCenter().getPanes().length).toBe 2 describe "tree-view:open-selected-entry", -> beforeEach -> atom.config.set "tree-view.alwaysOpenExisting", true describe "when the first pane is focused, a file is opened that is already open in the second pane", -> beforeEach -> - firstPane = atom.workspace.getPanes()[0] + firstPane = atom.workspace.getCenter().getPanes()[0] firstPane.activate() selectEntry 'tree-view.txt' - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch treeView.element, "tree-view:open-selected-entry" it "opens the file in the second pane and focuses it", -> - pane = atom.workspace.getPanes()[1] - item = atom.workspace.getActivePaneItem() + pane = atom.workspace.getCenter().getPanes()[1] + item = atom.workspace.getCenter().getActivePaneItem() expect(atom.views.getView(pane)).toHaveFocus() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') @@ -3264,14 +3718,14 @@ describe "TreeView", -> describe "when the first pane is focused, a file is opened that is already open in the second pane", -> firstPane = null beforeEach -> - firstPane = atom.workspace.getPanes()[0] + firstPane = atom.workspace.getCenter().getPanes()[0] firstPane.activate() selectEntry 'tree-view.txt' - waitsForFileToOpen -> + waitForWorkspaceOpenEvent -> atom.commands.dispatch treeView.element, "tree-view:open-selected-entry" it "opens the file in the first pane, which was the current focus", -> - item = atom.workspace.getActivePaneItem() + item = atom.workspace.getCenter().getActivePaneItem() expect(atom.views.getView(firstPane)).toHaveFocus() expect(item.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') @@ -3282,20 +3736,20 @@ describe "TreeView", -> describe "when core.allowPendingPaneItems is set to true (default)", -> firstPane = activePaneItem = null beforeEach -> - firstPane = atom.workspace.getPanes()[0] + firstPane = atom.workspace.getCenter().getPanes()[0] firstPane.activate() treeView.focus() - waitsForFileToOpen -> - sampleTxt.trigger clickEvent(originalEvent: {detail: 1}) + waitForWorkspaceOpenEvent -> + sampleTxt.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) runs -> - activePaneItem = atom.workspace.getActivePaneItem() + activePaneItem = atom.workspace.getCenter().getActivePaneItem() it "selects the file and retains focus on tree-view", -> expect(sampleTxt).toHaveClass 'selected' - expect(treeView).toHaveFocus() + expect(treeView.element).toHaveFocus() it "doesn't open the file in the active pane", -> expect(atom.views.getView(treeView)).toHaveFocus() @@ -3307,21 +3761,275 @@ describe "TreeView", -> activePaneItem = null beforeEach -> - firstPane = atom.workspace.getPanes()[0] + firstPane = atom.workspace.getCenter().getPanes()[0] firstPane.activate() treeView.focus() - waitsForFileToOpen -> - sampleTxt.trigger clickEvent(originalEvent: {detail: 1}) - sampleTxt.trigger clickEvent(originalEvent: {detail: 2}) + waitForWorkspaceOpenEvent -> + sampleTxt.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + sampleTxt.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 2})) waits 100 runs -> - activePaneItem = atom.workspace.getActivePaneItem() + activePaneItem = atom.workspace.getCenter().getActivePaneItem() it "opens the file and focuses it", -> expect(activePaneItem.getPath()).toBe atom.project.getDirectories()[0].resolve('tree-view.txt') - expect(atom.views.getView(atom.workspace.getPanes()[1])).toHaveFocus() + expect(atom.views.getView(atom.workspace.getCenter().getPanes()[1])).toHaveFocus() + + describe "Dragging and dropping root folders", -> + [alphaDirPath, gammaDirPath, thetaDirPath, etaDirPath] = [] + beforeEach -> + rootDirPath = fs.absolute(temp.mkdirSync('tree-view')) + + alphaFilePath = path.join(rootDirPath, "alpha.txt") + zetaFilePath = path.join(rootDirPath, "zeta.txt") + + alphaDirPath = path.join(rootDirPath, "alpha") + betaFilePath = path.join(alphaDirPath, "beta.txt") + + gammaDirPath = path.join(rootDirPath, "gamma") + deltaFilePath = path.join(gammaDirPath, "delta.txt") + epsilonFilePath = path.join(gammaDirPath, "epsilon.txt") + + thetaDirPath = path.join(rootDirPath, "theta") + etaDirPath = path.join(rootDirPath, "eta") + + fs.writeFileSync(alphaFilePath, "doesn't matter") + fs.writeFileSync(zetaFilePath, "doesn't matter") + + fs.makeTreeSync(alphaDirPath) + fs.writeFileSync(betaFilePath, "doesn't matter") + + fs.makeTreeSync(gammaDirPath) + fs.writeFileSync(deltaFilePath, "doesn't matter") + fs.writeFileSync(epsilonFilePath, "doesn't matter") + fs.makeTreeSync(thetaDirPath) + fs.makeTreeSync(etaDirPath) + + atom.project.setPaths([alphaDirPath, gammaDirPath, thetaDirPath]) + + jasmine.attachToDOM(workspaceElement) + + afterEach -> + [alphaDirPath, gammaDirPath, thetaDirPath, etaDirPath] = [] + + describe "when dragging a project root's header onto a different project root", -> + describe "when dragging on the top part of the root", -> + it "should add the placeholder above the directory", -> + # Dragging gammaDir onto alphaDir + alphaDir = treeView.roots[0] + gammaDir = treeView.roots[1] + [dragStartEvent, dragOverEvents, dragEndEvent] = + eventHelpers.buildPositionalDragEvents(gammaDir.querySelector('.project-root-header'), alphaDir, '.tree-view') + + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDragOver(dragOverEvents.top) + expect(alphaDir.previousSibling).toHaveClass('placeholder') + + # Is removed when drag ends + treeView.rootDragAndDrop.onDragEnd(dragEndEvent) + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when dragging on the bottom part of the root", -> + it "should add the placeholder below the directory", -> + # Dragging gammaDir onto alphaDir + alphaDir = treeView.roots[0] + gammaDir = treeView.roots[1] + [dragStartEvent, dragOverEvents, dragEndEvent] = + eventHelpers.buildPositionalDragEvents(gammaDir.querySelector('.project-root-header'), alphaDir, '.tree-view') + + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDragOver(dragOverEvents.bottom) + expect(alphaDir.nextSibling).toHaveClass('placeholder') + + # Is removed when drag ends + treeView.rootDragAndDrop.onDragEnd(dragEndEvent) + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when below all entries", -> + it "should add the placeholder below the last directory", -> + # Dragging gammaDir onto alphaDir + alphaDir = treeView.roots[0] + lastDir = treeView.roots[treeView.roots.length - 1] + [dragStartEvent, dragOverEvents, dragEndEvent] = + eventHelpers.buildPositionalDragEvents(alphaDir.querySelector('.project-root-header'), treeView.list) + + expect(alphaDir).not.toEqual(lastDir) + + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDragOver(dragOverEvents.bottom) + expect(lastDir.nextSibling).toHaveClass('placeholder') + + # Is removed when drag ends + treeView.rootDragAndDrop.onDragEnd(dragEndEvent) + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when dropping a project root's header onto a different project root", -> + describe "when dropping on the top part of the header", -> + it "should add the placeholder above the directory", -> + # dropping gammaDir above alphaDir + alphaDir = treeView.roots[0] + gammaDir = treeView.roots[1] + [dragStartEvent, dragDropEvents] = + eventHelpers.buildPositionalDragEvents(gammaDir.querySelector('.project-root-header'), alphaDir, '.tree-view') + + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDrop(dragDropEvents.top) + projectPaths = atom.project.getPaths() + expect(projectPaths[0]).toEqual(gammaDirPath) + expect(projectPaths[1]).toEqual(alphaDirPath) + + # Is removed when drag ends + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when dropping on the bottom part of the header", -> + it "should add the placeholder below the directory", -> + # dropping thetaDir below alphaDir + alphaDir = treeView.roots[0] + thetaDir = treeView.roots[2] + [dragStartEvent, dragDropEvents] = + eventHelpers.buildPositionalDragEvents(thetaDir.querySelector('.project-root-header'), alphaDir, '.tree-view') + + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDrop(dragDropEvents.bottom) + projectPaths = atom.project.getPaths() + expect(projectPaths[0]).toEqual(alphaDirPath) + expect(projectPaths[1]).toEqual(thetaDirPath) + expect(projectPaths[2]).toEqual(gammaDirPath) + + # Is removed when drag ends + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when a root folder is dragged out of application", -> + it "should carry the folder's information", -> + gammaDir = treeView.roots[1] + [dragStartEvent] = eventHelpers.buildPositionalDragEvents(gammaDir.querySelector('.project-root-header')) + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + + expect(dragStartEvent.dataTransfer.getData("text/plain")).toEqual gammaDirPath + if process.platform in ['darwin', 'linux'] + expect(dragStartEvent.dataTransfer.getData("text/uri-list")).toEqual "file://#{gammaDirPath}" + + describe "when a root folder is dropped from another Atom window", -> + it "adds the root folder to the window", -> + alphaDir = treeView.roots[0] + [_, dragDropEvents] = eventHelpers.buildPositionalDragEvents(null, alphaDir.querySelector('.project-root-header'), '.tree-view') + + dropEvent = dragDropEvents.bottom + dropEvent.dataTransfer.setData('atom-tree-view-event', true) + dropEvent.dataTransfer.setData('from-window-id', treeView.rootDragAndDrop.getWindowId() + 1) + dropEvent.dataTransfer.setData('from-root-path', etaDirPath) + + # mock browserWindowForId + browserWindowMock = {webContents: {send: ->}} + spyOn(remote.BrowserWindow, 'fromId').andReturn(browserWindowMock) + spyOn(browserWindowMock.webContents, 'send') + + treeView.rootDragAndDrop.onDrop(dropEvent) + + waitsFor -> + browserWindowMock.webContents.send.callCount > 0 + + runs -> + expect(atom.project.getPaths()).toContain etaDirPath + expect(document.querySelector('.placeholder')).not.toExist() + + + describe "when a root folder is dropped to another Atom window", -> + it "removes the root folder from the first window", -> + gammaDir = treeView.roots[1] + [dragStartEvent, dropEvent] = eventHelpers.buildPositionalDragEvents(gammaDir.querySelector('.project-root-header')) + treeView.rootDragAndDrop.onDragStart(dragStartEvent) + treeView.rootDragAndDrop.onDropOnOtherWindow({}, Array.from(gammaDir.parentElement.children).indexOf(gammaDir)) + + expect(atom.project.getPaths()).toEqual [alphaDirPath, thetaDirPath] + expect(document.querySelector('.placeholder')).not.toExist() + + describe "when there is a __proto__ entry present", -> + it "does not break anything", -> + # No assertions needed - multiple exceptions will be thrown if this test fails + projectPath = temp.mkdirSync('atom-project') + protoPath = path.join(projectPath, "__proto__") + fs.writeFileSync(protoPath, 'test') + atom.project.setPaths([projectPath]) + + describe "directory expansion serialization", -> + it "converts legacy expansion serialization Objects to Maps", -> + # The conversion actually happens when a new Directory + # is instantiated with a serialized expansion state, + # not when serialization occurs + legacyState = + isExpanded: true + entries: + 'a': + isExpanded: true + 'tree-view': + isExpanded: false + entries: + 'sub-folder': + isExpanded: true + + convertedState = + isExpanded: true + entries: new Map().set('a', {isExpanded: true}).set('tree-view', + isExpanded: false + entries: new Map().set 'sub-folder', + isExpanded: true) + + directory = new Directory({name: 'test', fullPath: 'path', symlink: false, expansionState: legacyState}) + expect(directory.expansionState.entries instanceof Map).toBe true + + assertEntriesDeepEqual = (expansionEntries, convertedEntries) -> + expansionEntries.forEach (entry, name) -> + if entry.entries? or convertedEntries.get(name).entries? + assertEntriesDeepEqual(entry.entries, convertedEntries.get(name).entries) + expect(entry).toEqual convertedEntries.get(name) + + assertEntriesDeepEqual(directory.expansionState.entries, convertedState.entries) + + findDirectoryContainingText = (element, text) -> + directories = Array.from(element.querySelectorAll('.entries .directory')) + directories.find((directory) -> directory.header.textContent is text) + + findFileContainingText = (element, text) -> + files = Array.from(element.querySelectorAll('.entries .file')) + files.find((file) -> file.fileName.textContent is text) + +describe 'Icon class handling', -> + it 'allows multiple classes to be passed', -> + rootDirPath = fs.absolute(temp.mkdirSync('tree-view-root1')) + + for i in [1..3] + filepath = path.join(rootDirPath, "file-#{i}.txt") + fs.writeFileSync(filepath, "Nah") + + atom.project.setPaths([rootDirPath]) + workspaceElement = atom.views.getView(atom.workspace) + + providerDisposable = atom.packages.serviceHub.provide 'atom.file-icons', '1.0.0', { + iconClassForPath: (path, context) -> + expect(context).toBe "tree-view" + [name, id] = path.match(/file-(\d+)\.txt$/) + switch id + when "1" then 'first-icon-class second-icon-class' + when "2" then ['third-icon-class', 'fourth-icon-class'] + else "some-other-file" + } + + waitForPackageActivation() + + runs -> + treeView = atom.packages.getActivePackage("tree-view").mainModule.getTreeViewInstance() + files = workspaceElement.querySelectorAll('li[is="tree-view-file"]') + + expect(files[0].fileName.className).toBe('name icon first-icon-class second-icon-class') + expect(files[1].fileName.className).toBe('name icon third-icon-class fourth-icon-class') + + providerDisposable.dispose() + + files = workspaceElement.querySelectorAll('li[is="tree-view-file"]') + expect(files[0].fileName.className).toBe('name icon icon-file-text') diff --git a/spec/tree-view-spec.js b/spec/tree-view-spec.js new file mode 100644 index 00000000..ece93794 --- /dev/null +++ b/spec/tree-view-spec.js @@ -0,0 +1,80 @@ +const TreeView = require('../lib/tree-view') + +describe('TreeView', () => { + describe('serialization', () => { + it('restores the expanded directories and selected file', () => { + const treeView = new TreeView({}) + treeView.roots[0].expand() + treeView.roots[0].entries.firstChild.expand() + treeView.selectEntry(treeView.roots[0].entries.firstChild.entries.firstChild) + + const treeView2 = new TreeView(treeView.serialize()) + + expect(treeView2.roots[0].isExpanded).toBe(true) + expect(treeView2.roots[0].entries.children[0].isExpanded).toBe(true) + expect(treeView2.roots[0].entries.children[1].isExpanded).toBeUndefined() + expect(Array.from(treeView2.getSelectedEntries())).toEqual([treeView2.roots[0].entries.firstChild.entries.firstChild]) + }) + + it('restores the scroll position', () => { + const treeView = new TreeView({}) + treeView.roots[0].expand() + treeView.roots[0].entries.firstChild.expand() + treeView.element.style.overflow = 'auto' + treeView.element.style.height = '80px' + treeView.element.style.width = '80px' + jasmine.attachToDOM(treeView.element) + + treeView.element.scrollTop = 42 + treeView.element.scrollLeft = 43 + + expect(treeView.element.scrollTop).toBe(42) + expect(treeView.element.scrollLeft).toBe(43) + + const treeView2 = new TreeView(treeView.serialize()) + treeView2.element.style.overflow = 'auto' + treeView2.element.style.height = '80px' + treeView2.element.style.width = '80px' + jasmine.attachToDOM(treeView2.element) + + waitsFor(() => + treeView2.element.scrollTop === 42 && + treeView2.element.scrollLeft === 43 + ) + }) + }) + + describe('clicking', () => { + it('should leave multiple entries selected on right click', () => { + const treeView = new TreeView({}) + const entries = treeView.roots[0].entries + + treeView.onMouseDown({ + stopPropagation() {}, + target: entries.children[0], + button: 0, + }) + + treeView.onMouseDown({ + stopPropagation() {}, + target: entries.children[1], + button: 0, + metaKey: true, + }) + + let child = entries.children[0]; + while (child.children.length > 0) { + child = child.firstChild; + } + + treeView.onMouseDown({ + stopPropagation() {}, + target: child, + button: 2, + }) + + expect(treeView.getSelectedEntries().length).toBe(2); + expect(treeView.multiSelectEnabled()).toBe(true); + }) + }); +}) diff --git a/styles/tree-view.less b/styles/tree-view.less index 7bdf6e5b..e37822b1 100644 --- a/styles/tree-view.less +++ b/styles/tree-view.less @@ -1,79 +1,53 @@ @import "ui-variables"; -.tree-view-resizer { - position: relative; - height: 100%; - overflow: hidden; - cursor: default; - -webkit-user-select: none; - min-width: 100px; - width: 200px; +.project-root-header { + -webkit-user-drag: element; +} + +.tree-view { + contain: size; + overflow: auto; z-index: 2; + -webkit-user-select: none; + display: flex; flex-direction: column; - // use these classes to re-order - // using a value in-between is fine too, e.g. order: -3; - & > .order--start { order: -10; } - & > .order--center { order: 0; } - & > .order--end { order: 10; } + .tree-view-root { + padding-left: @component-icon-padding; + padding-right: @component-padding; + background-color: inherit; - .tree-view-resize-handle { - position: absolute; - top: 0; - bottom: 0; - width: 10px; - cursor: col-resize; - z-index: 3; - } + /* + * Force a new stacking context to prevent a large, duplicate paint layer from + * being created for tree-view's scrolling contents that can make the cost of + * layer tree updates scale at 3x the size of the layer rather than the + * optimal 1x. + * + * On high resolution displays, Chromium handles layers for scrolling content + * differently and inadvertently creates a duplicate paint layer the size of + * .tree-view-scroller because descendants of the scroller overlap the + * auto-created layer. + */ + isolation: isolate; - &[data-show-on-right-side='true'] { - .tree-view-resize-handle { - left: -5px; - } - } + // Expands tree-view root to take up full height. + // This makes sure that the context menu can still be openend in the empty + // area underneath the files. + flex-grow: 1; - &[data-show-on-right-side='false'] { - .tree-view-resize-handle { - right: -5px; - } + // Expands tree-view root to take up as much width as needed by the content. + // This makes sure that the selected item's "bar/background" expands to full width. + position: relative; + min-width: min-content; } -} - -.tree-view-scroller { - display: flex; - flex-direction: column; - flex: 1; - width: 100%; - overflow: auto; -} - -.tree-view { - flex-grow: 1; - flex-shrink: 0; - /* - * Force a new stacking context to prevent a large, duplicate paint layer from - * being created for tree-view's scrolling contents that can make the cost of - * layer tree updates scale at 3x the size of the layer rather than the - * optimal 1x. - * - * On high resolution displays, Chromium handles layers for scrolling content - * differently and inadvertently creates a duplicate paint layer the size of - * .tree-view-scroller because descendants of the scroller overlap the - * auto-created layer. - */ - isolation: isolate; - min-width: -webkit-min-content; - min-height: 100%; - padding-left: @component-icon-padding; - padding-right: @component-padding; - position: relative; .header { position: relative; } - .list-tree { + .tree-view-root .list-tree { + // Keeps selections expanded while dragging position: static; } @@ -84,12 +58,47 @@ position: absolute; } } -} -.platform-win32 { - .tree-view-resizer { - .tree-view-resize-handle { - cursor: ew-resize; + /* Drag and Drop */ + .placeholder { + position: absolute; + left: @component-icon-padding; + padding: 0; + z-index: 999; + display: inline-block; + + width: calc(~"100% -" @component-icon-padding); + background: @background-color-info; + + list-style: none; + pointer-events: none; + + // bar + &:before { + content: ""; + position: absolute; + height: 2px; + margin: -1px; padding: 0; + width: inherit; + background: inherit; + } + + &:after { + content: ""; + position: absolute; + left: 0; + margin-top: -2px; + margin-left: -1px; + width: 4px; + height: 4px; + background: @background-color-info; + border-radius: 4px; + border: 1px solid transparent; + } + + // ensure that placeholder doesn't disappear above the top of the view + &:first-child { + margin-top: 1px; } } }