diff --git a/lib/tree-view.coffee b/lib/tree-view.coffee index 2001010e..a3bf2efb 100644 --- a/lib/tree-view.coffee +++ b/lib/tree-view.coffee @@ -690,68 +690,24 @@ class TreeView # Public: Paste a copied or cut item. # If a file is selected, the file's parent directory is used as the # paste destination. - # - # - # Returns `destination newPath`. pasteEntries: -> selectedEntry = @selectedEntry() + return unless selectedEntry + 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 + return unless initialPaths?.length - catchAndShowFileErrors = (operation) -> - try - operation() - catch error - atom.notifications.addWarning("Unable to paste paths: #{initialPaths}", detail: error.message) - - for initialPath in initialPaths ? [] - initialPathIsDirectory = fs.isDirectorySync(initialPath) - if selectedEntry and initialPath and fs.existsSync(initialPath) - basePath = selectedEntry.getPath() - basePath = path.dirname(basePath) if selectedEntry.classList.contains('file') - newPath = path.join(basePath, path.basename(initialPath)) - - # Do not allow copying test/a/ into test/a/b/ - # Note: A trailing path.sep is added to prevent false positives, such as test/a -> test/ab - realBasePath = fs.realpathSync(basePath) + path.sep - realInitialPath = fs.realpathSync(initialPath) + path.sep - if initialPathIsDirectory and realBasePath.startsWith(realInitialPath) - unless fs.isSymbolicLinkSync(initialPath) - atom.notifications.addWarning('Cannot paste a folder into itself') - continue + newDirectoryPath = selectedEntry.getPath() + newDirectoryPath = path.dirname(newDirectoryPath) if selectedEntry.classList.contains('file') + for initialPath in initialPaths + if fs.existsSync(initialPath) if copiedPaths - # append a number to the file if an item with the same name exists - fileCounter = 0 - originalNewPath = newPath - while fs.existsSync(newPath) - if initialPathIsDirectory - newPath = "#{originalNewPath}#{fileCounter}" - else - extension = getFullExtension(originalNewPath) - filePath = path.join(path.dirname(originalNewPath), path.basename(originalNewPath, extension)) - newPath = "#{filePath}#{fileCounter}#{extension}" - fileCounter += 1 - - if initialPathIsDirectory - # use fs.copy to copy directories since read/write will fail for directories - 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)) - @emitter.emit 'entry-copied', {initialPath, newPath} + @copyEntry(initialPath, newDirectoryPath) else if cutPaths - try - @emitter.emit 'will-move-entry', {initialPath, newPath} - fs.moveSync(initialPath, newPath) - @emitter.emit 'entry-moved', {initialPath, newPath} - catch error - @emitter.emit 'move-entry-failed', {initialPath, newPath} - atom.notifications.addWarning("Unable to paste paths: #{initialPaths}", detail: error.message) + break unless @moveEntry(initialPath, newDirectoryPath) add: (isCreatingFile) -> selectedEntry = @selectedEntry() ? @roots[0] @@ -832,10 +788,57 @@ class TreeView pageDown: -> @element.scrollTop += @element.offsetHeight - moveEntry: (initialPath, newDirectoryPath) -> - if initialPath is newDirectoryPath - return + # Copies an entry from `initialPath` to `newDirectoryPath` + # If the entry already exists in `newDirectoryPath`, a number is appended to the basename + copyEntry: (initialPath, newDirectoryPath) -> + initialPathIsDirectory = fs.isDirectorySync(initialPath) + # Do not allow copying test/a/ into test/a/b/ + # Note: A trailing path.sep is added to prevent false positives, such as test/a -> test/ab + realNewDirectoryPath = fs.realpathSync(newDirectoryPath) + path.sep + realInitialPath = fs.realpathSync(initialPath) + path.sep + if initialPathIsDirectory and realNewDirectoryPath.startsWith(realInitialPath) + unless fs.isSymbolicLinkSync(initialPath) + atom.notifications.addWarning('Cannot copy a folder into itself') + return + + newPath = path.join(newDirectoryPath, path.basename(initialPath)) + + # append a number to the file if an item with the same name exists + fileCounter = 0 + originalNewPath = newPath + while fs.existsSync(newPath) + if initialPathIsDirectory + newPath = "#{originalNewPath}#{fileCounter}" + else + extension = getFullExtension(originalNewPath) + filePath = path.join(path.dirname(originalNewPath), path.basename(originalNewPath, extension)) + newPath = "#{filePath}#{fileCounter}#{extension}" + fileCounter += 1 + + try + @emitter.emit 'will-copy-entry', {initialPath, newPath} + if initialPathIsDirectory + # use fs.copy to copy directories since read/write will fail for directories + fs.copySync(initialPath, newPath) + else + # read the old file and write a new one at target location + # TODO: Replace with fs.copyFileSync + fs.writeFileSync(newPath, fs.readFileSync(initialPath)) + @emitter.emit 'entry-copied', {initialPath, newPath} + + if repo = repoForPath(newPath) + repo.getPathStatus(initialPath) + repo.getPathStatus(newPath) + + catch error + @emitter.emit 'copy-entry-failed', {initialPath, newPath} + atom.notifications.addWarning("Failed to copy entry #{initialPath} to #{newDirectoryPath}", detail: error.message) + + # Moves an entry from `initialPath` to `newDirectoryPath` + moveEntry: (initialPath, newDirectoryPath) -> + # Do not allow moving test/a/ into test/a/b/ + # Note: A trailing path.sep is added to prevent false positives, such as test/a -> test/ab realNewDirectoryPath = fs.realpathSync(newDirectoryPath) + path.sep realInitialPath = fs.realpathSync(initialPath) + path.sep if fs.isDirectorySync(initialPath) and realNewDirectoryPath.startsWith(realInitialPath) @@ -843,12 +846,10 @@ class TreeView atom.notifications.addWarning('Cannot move a folder into itself') return - entryName = path.basename(initialPath) - newPath = path.join(newDirectoryPath, entryName) + newPath = path.join(newDirectoryPath, path.basename(initialPath)) try @emitter.emit 'will-move-entry', {initialPath, newPath} - fs.makeTreeSync(newDirectoryPath) unless fs.existsSync(newDirectoryPath) fs.moveSync(initialPath, newPath) @emitter.emit 'entry-moved', {initialPath, newPath} @@ -1144,12 +1145,18 @@ class TreeView # being moved or deleted # TODO: This can be removed when tree-view is switched to @atom/watcher @entryForPath(initialPath)?.collapse?() - break unless @moveEntry(initialPath, newDirectoryPath) + if (process.platform is 'darwin' and e.metaKey) or e.ctrlKey + @copyEntry(initialPath, newDirectoryPath) + else + break unless @moveEntry(initialPath, newDirectoryPath) else # Drop event from OS entry.classList.remove('selected') for file in e.dataTransfer.files - break unless @moveEntry(file.path, newDirectoryPath) + if (process.platform is 'darwin' and e.metaKey) or e.ctrlKey + @copyEntry(file.path, newDirectoryPath) + else + break unless @moveEntry(file.path, newDirectoryPath) else if e.dataTransfer.files.length # Drop event from OS that isn't targeting a folder: add a new project folder atom.project.addPath(entry.path) for entry in e.dataTransfer.files diff --git a/spec/event-helpers.coffee b/spec/event-helpers.coffee index 31af2dfa..0581f255 100644 --- a/spec/event-helpers.coffee +++ b/spec/event-helpers.coffee @@ -1,4 +1,4 @@ -module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget, treeView) -> +module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget, treeView, copy = false) -> dataTransfer = data: {} setData: (key, value) -> @data[key] = "#{value}" # Drag events stringify data values @@ -30,6 +30,9 @@ module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget, tree Object.defineProperty(dropEvent, 'target', value: dropTarget) Object.defineProperty(dropEvent, 'currentTarget', value: dropTarget) Object.defineProperty(dropEvent, 'dataTransfer', value: dataTransfer) + if copy + key = if process.platform is 'darwin' then 'metaKey' else 'ctrlKey' + Object.defineProperty(dropEvent, key, value: true) dragEnterEvent = new DragEvent('dragenter') Object.defineProperty(dragEnterEvent, 'target', value: enterTarget) @@ -38,7 +41,7 @@ module.exports.buildInternalDragEvents = (dragged, enterTarget, dropTarget, tree [dragStartEvent, dragEnterEvent, dropEvent] -module.exports.buildExternalDropEvent = (filePaths, dropTarget) -> +module.exports.buildExternalDropEvent = (filePaths, dropTarget, copy = false) -> dataTransfer = data: {} setData: (key, value) -> @data[key] = "#{value}" # Drag events stringify data values @@ -61,6 +64,9 @@ module.exports.buildExternalDropEvent = (filePaths, dropTarget) -> Object.defineProperty(dropEvent, 'target', value: dropTarget) Object.defineProperty(dropEvent, 'currentTarget', value: dropTarget) Object.defineProperty(dropEvent, 'dataTransfer', value: dataTransfer) + if copy + key = if process.platform is 'darwin' then 'metaKey' else 'ctrlKey' + Object.defineProperty(dropEvent, key, value: true) for filePath in filePaths dropEvent.dataTransfer.files.push({path: filePath}) diff --git a/spec/tree-view-package-spec.coffee b/spec/tree-view-package-spec.coffee index 7d4deb40..1ad8f4cd 100644 --- a/spec/tree-view-package-spec.coffee +++ b/spec/tree-view-package-spec.coffee @@ -1571,67 +1571,66 @@ describe "TreeView", -> LocalStorage.clear() atom.notifications.clear() - for operation in ['copy', 'cut'] - describe "when attempting to #{operation} and paste a directory into itself", -> - it "shows a warning notification and does not paste", -> - # /dir-1/ -> /dir-1/ - LocalStorage["tree-view:#{operation}Path"] = JSON.stringify([dirPath]) - 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)).toBe false - expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot paste a folder into itself' - - describe "when attempting to #{operation} and paste a directory into a nested child directory", -> - it "shows a warning notification and does not paste", -> - nestedPath = path.join(dirPath, 'nested') - fs.makeTreeSync(nestedPath) - - # /dir-1/ -> /dir-1/nested/ - LocalStorage["tree-view:#{operation}Path"] = JSON.stringify([dirPath]) - newPath = path.join(nestedPath, path.basename(dirPath)) - dirView.reload() - nestedView = dirView.querySelector('.directory') - nestedView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() - expect(fs.existsSync(newPath)).toBe false - expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot paste a folder into itself' - - describe "when attempting to #{operation} and paste a directory into a sibling directory that starts with the same letter", -> - it "allows the paste to occur", -> - # /dir-1/ -> /dir-2/ - LocalStorage["tree-view:#{operation}Path"] = JSON.stringify([dirPath]) - newPath = path.join(dirPath2, path.basename(dirPath)) - dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() - expect(fs.existsSync(newPath)).toBe true - expect(atom.notifications.getNotifications()[0]).toBeUndefined() - - describe "when attempting to #{operation} and paste a directory into a symlink of itself", -> - it "shows a warning notification and does not paste", -> - fs.symlinkSync(dirPath, path.join(rootDirPath, 'symdir'), 'junction') - - # /dir-1/ -> symlink of /dir-1/ - LocalStorage["tree-view:#{operation}Path"] = JSON.stringify([dirPath]) - newPath = path.join(dirPath, path.basename(dirPath)) - symlinkView = root1.querySelector('.directory') - symlinkView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() - expect(fs.existsSync(newPath)).toBe false - expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot paste a folder into itself' - - describe "when attempting to #{operation} and paste a symlink into its target directory", -> - it "allows the paste to occur", -> - symlinkedPath = path.join(rootDirPath, 'symdir') - fs.symlinkSync(dirPath, symlinkedPath, 'junction') - - # symlink of /dir-1/ -> /dir-1/ - LocalStorage["tree-view:#{operation}Path"] = JSON.stringify([symlinkedPath]) - newPath = path.join(dirPath, path.basename(symlinkedPath)) - dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) - expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() - expect(fs.existsSync(newPath)).toBe true - expect(atom.notifications.getNotifications()[0]).toBeUndefined() + describe "when attempting to paste a directory into itself", -> + it "shows a warning notification and does not paste", -> + # /dir-1/ -> /dir-1/ + LocalStorage["tree-view:copyPath"] = JSON.stringify([dirPath]) + 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)).toBe false + expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot copy a folder into itself' + + describe "when attempting to paste a directory into a nested child directory", -> + it "shows a warning notification and does not paste", -> + nestedPath = path.join(dirPath, 'nested') + fs.makeTreeSync(nestedPath) + + # /dir-1/ -> /dir-1/nested/ + LocalStorage["tree-view:copyPath"] = JSON.stringify([dirPath]) + newPath = path.join(nestedPath, path.basename(dirPath)) + dirView.reload() + nestedView = dirView.querySelector('.directory') + nestedView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() + expect(fs.existsSync(newPath)).toBe false + expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot copy a folder into itself' + + describe "when attempting to paste a directory into a sibling directory that starts with the same letter", -> + it "allows the paste to occur", -> + # /dir-1/ -> /dir-2/ + LocalStorage["tree-view:copyPath"] = JSON.stringify([dirPath]) + newPath = path.join(dirPath2, path.basename(dirPath)) + dirView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() + expect(fs.existsSync(newPath)).toBe true + expect(atom.notifications.getNotifications()[0]).toBeUndefined() + + describe "when attempting to paste a directory into a symlink of itself", -> + it "shows a warning notification and does not paste", -> + fs.symlinkSync(dirPath, path.join(rootDirPath, 'symdir'), 'junction') + + # /dir-1/ -> symlink of /dir-1/ + LocalStorage["tree-view:copyPath"] = JSON.stringify([dirPath]) + newPath = path.join(dirPath, path.basename(dirPath)) + symlinkView = root1.querySelector('.directory') + symlinkView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() + expect(fs.existsSync(newPath)).toBe false + expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Cannot copy a folder into itself' + + describe "when attempting to paste a symlink into its target directory", -> + it "allows the paste to occur", -> + symlinkedPath = path.join(rootDirPath, 'symdir') + fs.symlinkSync(dirPath, symlinkedPath, 'junction') + + # symlink of /dir-1/ -> /dir-1/ + LocalStorage["tree-view:copyPath"] = JSON.stringify([symlinkedPath]) + newPath = path.join(dirPath, path.basename(symlinkedPath)) + dirView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + expect(-> atom.commands.dispatch(treeView.element, "tree-view:paste")).not.toThrow() + expect(fs.existsSync(newPath)).toBe true + expect(atom.notifications.getNotifications()[0]).toBeUndefined() describe "when pasting entries which don't exist anymore", -> it "skips the entry which doesn't exist", -> @@ -1845,7 +1844,9 @@ describe "TreeView", -> expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath}) describe 'when the target destination file exists', -> - it 'emits a warning and does not move the cut file', -> + it "prompts to replace the file", -> + spyOn(atom, 'confirm') + callback = jasmine.createSpy("onEntryMoved") treeView.onEntryMoved(callback) @@ -1855,11 +1856,55 @@ describe "TreeView", -> 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() + expect(atom.confirm).toHaveBeenCalled() + + describe "when selecting the replace option", -> + it "replaces the existing file", -> + spyOn(atom, 'confirm').andReturn 0 + + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + + filePath3 = path.join(dirPath2, "test-file.txt") + fs.writeFileSync(filePath3, "doesn't matter") + + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + expect(fs.existsSync(filePath)).toBe(false) + expect(callback).toHaveBeenCalledWith({initialPath: filePath, newPath: filePath3}) + + describe "when selecting the skip option", -> + it "does not replace the existing file", -> + spyOn(atom, 'confirm').andReturn 1 + + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + + filePath3 = path.join(dirPath2, "test-file.txt") + fs.writeFileSync(filePath3, "doesn't matter") + + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + expect(fs.existsSync(filePath)).toBe(true) + expect(callback).not.toHaveBeenCalled() + + describe "when cancelling the dialog", -> + it "does not replace the existing file", -> + spyOn(atom, 'confirm').andReturn 2 + + callback = jasmine.createSpy("onEntryMoved") + treeView.onEntryMoved(callback) + + filePath3 = path.join(dirPath2, "test-file.txt") + fs.writeFileSync(filePath3, "doesn't matter") + + fileView2.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(atom.notifications.getNotifications().length).toBe(1) - expect(atom.notifications.getNotifications()[0].getMessage()).toContain('Unable to paste') + expect(fs.existsSync(filePath)).toBe(true) + expect(callback).not.toHaveBeenCalled() describe 'when the file is currently open', -> beforeEach -> @@ -1931,20 +1976,62 @@ describe "TreeView", -> 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]) + describe "when the target destination file exists", -> + filePath5 = null - filePath4 = path.join(dirPath, "test-file2.txt") - filePath5 = path.join(dirPath, "test-file3.txt") - fs.writeFileSync(filePath4, "doesn't matter") - fs.writeFileSync(filePath5, "doesn't matter") + beforeEach -> + filePath5 = path.join(dirPath2, "test-file.txt") # So that dirPath2 has an exact copy of files in dirPath + filePath6 = path.join(dirPath, "test-file2.txt") + filePath7 = path.join(dirPath, "test-file3.txt") + fs.writeFileSync(filePath5, "doesn't matter 5") + fs.writeFileSync(filePath6, "doesn't matter 6") + fs.writeFileSync(filePath7, "doesn't matter 7") + + LocalStorage['tree-view:cutPath'] = JSON.stringify([filePath5, filePath2, filePath3]) + + it "prompts for each file as long as cancel is not chosen", -> + calls = 0 + getButton = -> + calls++ + switch calls + when 1 + return 0 + when 2 + return 1 + when 3 + return 0 + + spyOn(atom, 'confirm').andCallFake -> getButton() fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(fs.existsSync(filePath2)).toBeTruthy() - expect(fs.existsSync(filePath3)).toBeTruthy() + expect(atom.confirm.calls.length).toBe(3) + + expect(fs.existsSync(filePath5)).toBe(false) + expect(fs.existsSync(filePath2)).toBe(true) + expect(fs.existsSync(filePath3)).toBe(false) + + it "immediately cancels any pending file moves when cancel is chosen", -> + calls = 0 + getButton = -> + calls++ + switch calls + when 1 + return 0 + when 2 + return 2 + + spyOn(atom, 'confirm').andCallFake -> getButton() + + fileView.dispatchEvent(new MouseEvent('click', {bubbles: true, detail: 1})) + atom.commands.dispatch(treeView.element, "tree-view:paste") + + expect(atom.confirm.calls.length).toBe(2) + + expect(fs.existsSync(filePath5)).toBe(false) + expect(fs.existsSync(filePath2)).toBe(true) + expect(fs.existsSync(filePath3)).toBe(true) describe "when a directory is selected", -> it "creates a copy of the original file in the selected directory and removes the original", -> @@ -1970,7 +2057,7 @@ describe "TreeView", -> atom.notifications.clear() atom.commands.dispatch(treeView.element, "tree-view:paste") - expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Unable to paste paths' + expect(atom.notifications.getNotifications()[0].getMessage()).toContain 'Failed to copy entry' expect(atom.notifications.getNotifications()[0].getDetail()).toContain 'ENOENT: no such file or directory' describe "tree-view:add-file", -> @@ -3832,6 +3919,18 @@ describe "TreeView", -> deltaFilePath, epsilonFilePath, thetaDirPath, thetaFilePath] = [] beforeEach -> + # tree-view + # ├── alpha/ + # │   ├── beta.txt + # │   └── eta/ + # ├── alpha.txt + # ├── gamma/ + # │   ├── delta.txt + # │   ├── epsilon.txt + # │   └── theta/ + # │      └── theta.txt + # └── zeta.txt + rootDirPath = fs.absolute(temp.mkdirSync('tree-view')) alphaFilePath = path.join(rootDirPath, "alpha.txt") @@ -3932,6 +4031,9 @@ describe "TreeView", -> gammaDir.expand() deltaFile = gammaDir.entries.children[1] + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + gammaDirContents = findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length + [dragStartEvent, dragEnterEvent, dropEvent] = eventHelpers.buildInternalDragEvents([deltaFile], alphaDir.querySelector('.header'), alphaDir, treeView) @@ -3940,10 +4042,39 @@ describe "TreeView", -> expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents and + findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length < gammaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + expect(findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length).toBe gammaDirContents - 1 + + describe 'when the ctrl/cmd modifier key is pressed', -> + it "should copy the file 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() + deltaFile = gammaDir.entries.children[1] + + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + gammaDirContents = findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length + + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents([deltaFile], alphaDir.querySelector('.header'), alphaDir, treeView, true) + + 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 > alphaDirContents + + runs -> + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + expect(findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length).toBe gammaDirContents it "shouldn't update editors with similar file paths", -> deltaFilePath2 = path.join(gammaDirPath, 'delta.txt2') @@ -3986,6 +4117,9 @@ describe "TreeView", -> gammaDir.expand() deltaFile = gammaDir.entries.children[1] + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + gammaDirContents = findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length + [dragStartEvent, dragEnterEvent, dropEvent] = eventHelpers.buildInternalDragEvents([deltaFile], betaFile, alphaDir, treeView) @@ -3994,10 +4128,12 @@ describe "TreeView", -> expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents and + findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length < gammaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + expect(findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length).toBe gammaDirContents - 1 it "shouldn't update editors with similar file paths", -> deltaFilePath2 = path.join(gammaDirPath, 'delta.txt2') @@ -4032,7 +4168,7 @@ describe "TreeView", -> describe "when dropping multiple FileViews onto a DirectoryView's header", -> it "should move the files to the hovered directory", -> - # Dragging delta.txt onto alphaDir + # Dragging multiple files in gammaDir onto alphaDir alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') alphaDir.expand() @@ -4040,6 +4176,9 @@ describe "TreeView", -> gammaDir.expand() gammaFiles = [].slice.call(gammaDir.entries.children, 1, 3) + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + gammaDirContents = findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length + [dragStartEvent, dragEnterEvent, dropEvent] = eventHelpers.buildInternalDragEvents(gammaFiles, alphaDir.querySelector('.header'), alphaDir, treeView) @@ -4049,10 +4188,12 @@ describe "TreeView", -> expect(alphaDir.entries.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents and + findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length < gammaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 4 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 2 + expect(findDirectoryContainingText(treeView.roots[0], 'gamma').querySelectorAll('.entry').length).toBe gammaDirContents - 2 describe "when dropping a DirectoryView and FileViews onto a DirectoryView's header", -> it "should move the files and directory to the hovered directory", -> @@ -4066,6 +4207,9 @@ describe "TreeView", -> thetaDir = findDirectoryContainingText(treeView.roots[0], 'theta') thetaDir.expand() + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + thetaDirContents = findDirectoryContainingText(treeView.roots[0], 'theta').querySelectorAll('.entry').length + dragged = [alphaFile, alphaDir] [dragStartEvent, dragEnterEvent, dropEvent] = @@ -4077,15 +4221,15 @@ describe "TreeView", -> expect(thetaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'theta').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'theta').querySelectorAll('.entry').length > thetaDirContents runs -> thetaDir.expand() - expect(thetaDir.querySelectorAll('.entry').length).toBe 3 + expect(thetaDir.querySelectorAll('.entry').length).toBe thetaDirContents + 2 # alpha dir still has all its entries alphaDir = findDirectoryContainingText(thetaDir.entries, 'alpha') alphaDir.expand() - expect(alphaDir.querySelectorAll('.entry').length).toBe 2 + expect(alphaDir.querySelectorAll('.entry').length).toBe alphaDirContents describe "when dropping a DirectoryView onto a DirectoryView's header", -> beforeEach -> @@ -4102,6 +4246,9 @@ describe "TreeView", -> thetaDir = gammaDir.entries.children[0] thetaDir.expand() + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + thetaDirContents = findDirectoryContainingText(treeView.roots[0], 'theta').querySelectorAll('.entry').length + [dragStartEvent, dragEnterEvent, dropEvent] = eventHelpers.buildInternalDragEvents([thetaDir], alphaDir.querySelector('.header'), alphaDir, treeView) treeView.onDragStart(dragStartEvent) @@ -4109,10 +4256,15 @@ describe "TreeView", -> expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + + thetaDir = findDirectoryContainingText(alphaDir.entries, 'theta') + thetaDir.expand() + expect(thetaDir.querySelectorAll('.entry').length).toBe thetaDirContents + editor = atom.workspace.getActiveTextEditor() expect(editor.getPath()).toBe(thetaFilePath.replace('gamma', 'alpha')) @@ -4220,7 +4372,7 @@ describe "TreeView", -> expect(atom.notifications.getNotifications()[0]).toBeUndefined() 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", -> + it "should not move the files and directory", -> # Dragging alpha.txt and alphaDir into alphaDir alphaFile = treeView.roots[0].entries.children[2] alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') @@ -4237,22 +4389,76 @@ describe "TreeView", -> treeView.onDrop(dropEvent) expect(treeView.moveEntry).not.toHaveBeenCalled() + describe "when dropping a DirectoryView and FileViews in the same parent DirectoryView", -> + describe "when the ctrl/cmd modifier key is pressed", -> + it "should copy the files and directory", -> + # Dragging beta.txt and etaDir into alphaDir + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + betaFile = alphaDir.entries.children[0] + etaDir = alphaDir.entries.children[1] + + dragged = [betaFile, etaDir] + + alphaDirContents = alphaDir.querySelectorAll('.entry').length + + [dragStartEvent, dragEnterEvent, dropEvent] = + eventHelpers.buildInternalDragEvents(dragged, alphaDir.querySelector('.header'), alphaDir, treeView, true) + + spyOn(treeView, 'copyEntry').andCallThrough() + + treeView.onDragStart(dragStartEvent) + treeView.onDrop(dropEvent) + expect(treeView.copyEntry).toHaveBeenCalled() + + waitsFor "directory view contents to refresh", -> + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents + + runs -> + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 2 + expect(fs.existsSync(path.join(alphaDirPath, 'beta0.txt'))).toBe true + expect(fs.existsSync(path.join(alphaDirPath, 'eta0'))).toBe true + 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 = findDirectoryContainingText(treeView.roots[0], 'alpha') alphaDir.expand() + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath], alphaDir) treeView.onDrop(dropEvent) expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + expect(fs.existsSync(deltaFilePath)).toBe false + + describe "when the ctrl/cmd modifier key is pressed", -> + it "should copy the file to the hovered directory", -> + # Dragging delta.txt from OS file explorer onto alphaDir + alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') + alphaDir.expand() + + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + + dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath], alphaDir, true) + + runs -> + treeView.onDrop(dropEvent) + expect(alphaDir.children.length).toBe 2 + + waitsFor "directory view contents to refresh", -> + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents + + runs -> + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 + expect(fs.existsSync(deltaFilePath)).toBe true describe "when dragging a directory from the OS onto a DirectoryView's header", -> it "should move the directory to the hovered directory", -> @@ -4260,15 +4466,17 @@ describe "TreeView", -> alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') alphaDir.expand() + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + dropEvent = eventHelpers.buildExternalDropEvent([gammaDirPath], alphaDir) treeView.onDrop(dropEvent) expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 2 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 3 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 1 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", -> @@ -4276,16 +4484,18 @@ describe "TreeView", -> alphaDir = findDirectoryContainingText(treeView.roots[0], 'alpha') alphaDir.expand() + alphaDirContents = findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length + dropEvent = eventHelpers.buildExternalDropEvent([deltaFilePath, gammaDirPath], alphaDir) treeView.onDrop(dropEvent) expect(alphaDir.children.length).toBe 2 waitsFor "directory view contents to refresh", -> - findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > 3 + findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length > alphaDirContents runs -> - expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe 4 + expect(findDirectoryContainingText(treeView.roots[0], 'alpha').querySelectorAll('.entry').length).toBe alphaDirContents + 2 describe "when dragging a directory from the OS onto a blank section of the Tree View", -> it "should create a new project folder", ->