Skip to content

Commit

Permalink
Merge pull request atom#1257 from atom/wl-copy-on-ctrl
Browse files Browse the repository at this point in the history
Copy files on drop if control is pressed
  • Loading branch information
Nathan Sobo authored May 14, 2019
2 parents a0fd2d9 + 9dce9f2 commit 573bb39
Show file tree
Hide file tree
Showing 3 changed files with 381 additions and 158 deletions.
129 changes: 68 additions & 61 deletions lib/tree-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -832,23 +788,68 @@ 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)
unless fs.isSymbolicLinkSync(initialPath)
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}

Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions spec/event-helpers.coffee
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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})
Expand Down
Loading

0 comments on commit 573bb39

Please sign in to comment.