aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--background_scripts/main.coffee4
-rw-r--r--content_scripts/mode_visual_edit.coffee197
-rw-r--r--lib/clipboard.coffee4
3 files changed, 127 insertions, 78 deletions
diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee
index c1c8dfc8..37d219df 100644
--- a/background_scripts/main.coffee
+++ b/background_scripts/main.coffee
@@ -168,9 +168,10 @@ upgradeNotificationClosed = (request) ->
sendRequestToAllTabs({ name: "hideUpgradeNotification" })
#
-# Copies some data (request.data) to the clipboard.
+# Copies or pastes some data (request.data) to/from the clipboard.
#
copyToClipboard = (request) -> Clipboard.copy(request.data)
+pasteFromClipboard = (request) -> Clipboard.paste()
#
# Selects the tab with the ID specified in request.id
@@ -647,6 +648,7 @@ sendRequestHandlers =
upgradeNotificationClosed: upgradeNotificationClosed
updateScrollPosition: handleUpdateScrollPosition
copyToClipboard: copyToClipboard
+ pasteFromClipboard: pasteFromClipboard
isEnabledForUrl: isEnabledForUrl
saveHelpDialogSettings: saveHelpDialogSettings
selectSpecificTab: selectSpecificTab
diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee
index d32ae24c..d870a69d 100644
--- a/content_scripts/mode_visual_edit.coffee
+++ b/content_scripts/mode_visual_edit.coffee
@@ -63,7 +63,16 @@ character = "character"
class Movement extends MaintainCount
opposite: { forward: backward, backward: forward }
- # Call a function. Return true if the selection changed.
+ copy: (text) ->
+ chrome.runtime.sendMessage
+ handler: "copyToClipboard"
+ data: text
+
+ paste: (callback) ->
+ chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) ->
+ callback response
+
+ # Call a function. Return true if the selection changed, false otherwise.
selectionChanged: (func) ->
r = @selection.getRangeAt(0).cloneRange()
func()
@@ -99,6 +108,20 @@ class Movement extends MaintainCount
@selection[if direction == forward then "collapseToEnd" else "collapseToStart"]()
@selection.modify "extend", @opposite[direction], character for [0...length]
+ protectClipboard: do ->
+ locked = false
+
+ (func) ->
+ if @alterMethod == "move" and not locked
+ locked = true
+ @paste (text) =>
+ result = func()
+ @copy text
+ locked = false
+ result
+ else
+ func()
+
# Run a movement command. Return true if the selection changed, false otherwise.
runMovement: (movement) ->
@selectionChanged => @selection.modify @alterMethod, movement.split(" ")...
@@ -124,9 +147,11 @@ class Movement extends MaintainCount
"0": "backward lineboundary"
"G": "forward documentboundary"
"gg": "backward documentboundary"
+ "Y": -> @selectLine()
"o": -> @swapFocusAndAnchor()
constructor: (options) ->
+ @selection = window.getSelection()
@movements = extend {}, @movements
@commands = {}
@alterMethod = options.alterMethod || "extend"
@@ -134,6 +159,15 @@ class Movement extends MaintainCount
@yankedText = ""
super extend options
+ # Aliases.
+ @movements.B = @movements.b
+ @movements.W = @movements.w
+
+ if @options.runMovement
+ @handleMovementKeyChar @options.runMovement
+ @yank()
+ return
+
@push
_name: "#{@id}/keypress"
keypress: (event) =>
@@ -147,42 +181,66 @@ class Movement extends MaintainCount
@keyQueue = ""
@selection = window.getSelection()
+ # If there's matching a command *and* a matching movement, then choose the command.
if @commands[keyChar]
@commands[keyChar].call @
@scrollIntoView()
return @suppressEvent
else if @movements[keyChar]
- @runCountPrefixTimes =>
- switch typeof @movements[keyChar]
- when "string" then @runMovement @movements[keyChar]
- when "function" then @movements[keyChar].call @
- @scrollIntoView()
- if @options.singleMovement
+ @handleMovementKeyChar keyChar
+
+ if @options.onYank or @options.oneMovementOnly
+ @scrollIntoView()
@yank()
return @suppressEvent
+ @scrollIntoView()
+ break
+
@continueBubbling
- # Aliases.
- @movements.B = @movements.b
- @movements.W = @movements.w
+ handleMovementKeyChar: (keyChar) ->
+ if @movements[keyChar]
+ @protectClipboard =>
+ @runCountPrefixTimes =>
+ switch typeof @movements[keyChar]
+ when "string" then @runMovement @movements[keyChar]
+ when "function" then @movements[keyChar].call @
yank: (args = {}) ->
- @yankedText = text = window.getSelection().toString()
- console.log "yank:", text
- @selection.deleteFromDocument() if args.deleteFromDocument
- @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()
+ @yankedText = window.getSelection().toString()
+ @selection.deleteFromDocument() if args.deleteFromDocument or @options.deleteFromDocument
+ console.log "yank:", @yankedText
+
+ text = @yankedText.replace /\s+/g, " "
+ length = text.length
+ text = text[...12] + "..." if 15 < length
+ HUD.showForDuration "Yanked #{length} character#{if length == 1 then "" else "s"}: \"#{text}\".", 2500
+
+ @exit()
@yankedText
- yankLine: ->
+ exit: (event) ->
+ super()
+ unless @options.underEditMode
+ if document.activeElement and DomUtils.isEditable document.activeElement
+ document.activeElement.blur()
+ if 0 < @selection.toString().length
+ @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()
+ @options.onYank.call @ if @options.onYank
+ # Because the various selection operations can mess with the clipboard, this must be the very-last thing
+ # we do.
+ @copy @yankedText if @yankedText
+
+ selectLine: ->
for direction in [ forward, backward ]
@runMovement "#{direction} lineboundary"
@swapFocusAndAnchor()
- @lastYankedLine = @yank()
- enterInsertMode: ->
- new InsertMode { badge: "I", blurOnEscape: false }
+ yankLine: ->
+ @selectLine()
+ @lastYankedLine = @yank()
# Adapted from: http://roysharon.com/blog/37.
# I have no idea how this works (smblott, 2015/1/22).
@@ -202,26 +260,27 @@ class Movement extends MaintainCount
# Try to scroll the focus into view.
scrollIntoView: ->
- if document.activeElement and DomUtils.isEditable document.activeElement
+ @protectClipboard =>
element = document.activeElement
- if element.clientHeight < element.scrollHeight
- if element.isContentEditable
- # How do we do this?
- else
- coords = DomUtils.getCaretCoordinates element, element.selectionStart
- Scroller.scrollToPosition element, coords.top, coords.left
- else
- # getElementWithFocus() seems to work most (but not all) of the time.
- leadingElement = @getElementWithFocus @selection
- Scroller.scrollIntoView leadingElement if leadingElement
+ if element and DomUtils.isEditable element
+ if element.clientHeight < element.scrollHeight
+ if element.isContentEditable
+ # How do we do this?
+ else
+ position = if @getDirection() == backward then element.selectionStart else element.selectionEnd
+ coords = DomUtils.getCaretCoordinates element, position
+ Scroller.scrollToPosition element, coords.top, coords.left
+ else
+ # getElementWithFocus() seems to work most (but not all) of the time.
+ leadingElement = @getElementWithFocus @selection
+ Scroller.scrollIntoView leadingElement if leadingElement
class VisualMode extends Movement
constructor: (options = {}) ->
@selection = window.getSelection()
-
switch @selection.type
when "None"
- HUD.showForDuration "An initial selection is required for visual mode.", 2500
+ HUD.showForDuration "Create a selection before entering visual mode.", 2500
return
when "Caret"
# Try to start with a visible selection.
@@ -242,32 +301,12 @@ class VisualMode extends Movement
if @options.underEditMode
extend @commands,
"d": => @yank deleteFromDocument: true
- "c": => @yank deleteFromDocument: true; @enterInsertMode()
-
- yank: (args...) ->
- text = super args...
- length = text.length
- text = text.replace /\s+/g, " "
- text = text[...12] + "..." if 15 < length
- HUD.showForDuration "Yanked #{length} character#{if length == 1 then "" else "s"}: \"#{text}\".", 2500
- @exit()
-
- exit: (event) ->
- super()
- unless @options.underEditMode
- if document.activeElement and DomUtils.isEditable document.activeElement
- document.activeElement.blur()
- # Now set the clipboard. No operations which maniplulate the selection should follow this.
- chrome.runtime.sendMessage { handler: "copyToClipboard", data: @yankedText } if @yankedText
+ "c": => @yank deleteFromDocument: true; enterInsertMode()
class VisualModeForEdit extends VisualMode
constructor: (options = {}) ->
super extend options, underEditMode: true
- exit: (args...) ->
- @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()
- super args...
-
class EditMode extends Movement
constructor: (options = {}) ->
@element = document.activeElement
@@ -280,50 +319,60 @@ class EditMode extends Movement
alterMethod: "move"
extend @commands,
- "i": @enterInsertMode
- "a": @enterInsertMode
- "A": => @runMovement "forward lineboundary"; @enterInsertMode()
+ "i": enterInsertMode
+ "a": enterInsertMode
+ "A": => @runMovement "forward lineboundary"; enterInsertMode()
"o": => @openLine forward
"O": => @openLine backward
"p": => @pasteClipboard forward
"P": => @pasteClipboard backward
"v": -> new VisualModeForEdit
- "Y": => @withRangeSelection => @yankLine()
- "y": =>
- new VisualModeForEdit
- singleMovement: true
- initialCount: @countPrefix
+
+ "Y": -> @runInVisualMode runMovement: "Y"
+ "y": => @runInVisualMode()
+ "d": => @runInVisualMode deleteFromDocument: true
+ "c": => @runInVisualMode
+ deleteFromDocument: true
+ onYank: -> new InsertMode { badge: "I", blurOnEscape: false }
+
+ "D": => @runInVisualMode runMovement: "$", deleteFromDocument: true
+ "C": => @runInVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode
# # Aliases.
# @commands.Y = @commands.yy
+ runInVisualMode: (options = {}) ->
+ defaults =
+ initialCount: @countPrefix
+ oneMovementOnly: true
+ new VisualModeForEdit extend defaults, options
+ @countPrefix = ""
+
pasteClipboard: (direction) ->
- text = Clipboard.paste @element
- if text
- if text == @lastYankedLine
- text += "\n"
- @runMovement "#{direction} lineboundary"
- @runMovement "#{direction} character" if direction == forward
- DomUtils.simulateTextEntry @element, text
+ @protectClipboard =>
+ @paste (text) =>
+ if text
+ if text == @lastYankedLine
+ text += "\n"
+ @runMovement "#{direction} lineboundary"
+ @runMovement "#{direction} character" if direction == forward
+ DomUtils.simulateTextEntry @element, text
openLine: (direction) ->
@runMovement "#{direction} lineboundary"
- @enterInsertMode()
+ enterInsertMode()
DomUtils.simulateTextEntry @element, "\n"
@runMovement "backward character" if direction == backward
- withRangeSelection: (func) ->
- @alterMethod = "extend"
- func.call @
- @alterMethod = "move"
- @selection.collapseToStart()
-
exit: (event, target) ->
super()
if event?.type = "keydown" and KeyboardUtils.isEscape event
if target? and DomUtils.isDOMDescendant @element, target
@element.blur()
+enterInsertMode = ->
+ new InsertMode { badge: "I", blurOnEscape: false }
+
root = exports ? window
root.VisualMode = VisualMode
root.EditMode = EditMode
diff --git a/lib/clipboard.coffee b/lib/clipboard.coffee
index 836b57e4..2b28df70 100644
--- a/lib/clipboard.coffee
+++ b/lib/clipboard.coffee
@@ -15,15 +15,13 @@ Clipboard =
document.execCommand("Copy")
document.body.removeChild(textArea)
- paste: (refocusElement = null) ->
+ paste: ->
textArea = @._createTextArea()
document.body.appendChild(textArea)
textArea.focus()
document.execCommand("Paste")
value = textArea.value
document.body.removeChild(textArea)
- # The caller wants this element refocused.
- refocusElement.focus() if refocusElement
value