diff options
| author | Stephen Blott | 2015-01-23 11:02:31 +0000 |
|---|---|---|
| committer | Stephen Blott | 2015-01-23 15:41:02 +0000 |
| commit | e1b7b0a963490b0991d72a0143f489e0bc1e8096 (patch) | |
| tree | dc10bf7994b1ccc259af65aea9c029908cccd7d3 | |
| parent | 256beee031efef70f4ee750044d9e697d66868bd (diff) | |
| download | vimium-e1b7b0a963490b0991d72a0143f489e0bc1e8096.tar.bz2 | |
Visual/edit modes: more (and better) commands.
| -rw-r--r-- | background_scripts/commands.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/mode_insert.coffee | 1 | ||||
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 267 | ||||
| -rw-r--r-- | lib/clipboard.coffee | 4 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 5 |
5 files changed, 151 insertions, 129 deletions
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index f79f495b..67c4b9ad 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -198,9 +198,6 @@ defaultKeyMappings = "i": "enterInsertMode" "v": "enterVisualMode" - # NOTE(smblott). We'll try two default bindings for enterEditMode, and see which ends up feeling mode - # natural. - "gv": "enterEditMode" "e": "enterEditMode" "H": "goBack" diff --git a/content_scripts/mode_insert.coffee b/content_scripts/mode_insert.coffee index 818c8408..741f36cd 100644 --- a/content_scripts/mode_insert.coffee +++ b/content_scripts/mode_insert.coffee @@ -10,7 +10,6 @@ class InsertMode extends Mode handleKeyEvent = (event) => return @continueBubbling unless @isActive event - console.log "key", String.fromCharCode(event.charCode) if event.type == 'keypress' return @stopBubblingAndTrue unless event.type == 'keydown' and KeyboardUtils.isEscape event DomUtils.suppressKeyupAfterEscape handlerStack target = event.srcElement diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index e6ea968a..e2e9c5fe 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -3,7 +3,6 @@ # through chrome keyboard shortcuts. It's a backstop for all of the modes following. class SuppressPrintable extends Mode constructor: (options) -> - handler = (event) => if KeyboardUtils.isPrintable event if event.type == "keydown" @@ -29,7 +28,7 @@ class SuppressPrintable extends Mode super options @onExit => handlerStack.remove @suppressPrintableHandlerId -# This watches keyboard events and maintains @countPrefix as number and other keys are pressed. +# This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. class MaintainCount extends SuppressPrintable constructor: (options) -> @countPrefix = "" @@ -51,6 +50,7 @@ class MaintainCount extends SuppressPrintable count = if 0 < @countPrefix.length then parseInt @countPrefix else 1 func() for [0...count] +# Some symbolic names. forward = "forward" backward = "backward" character = "character" @@ -58,56 +58,50 @@ character = "character" # This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit # mode. class Movement extends MaintainCount - - opposite: - forward: backward - backward: forward + opposite: { forward: backward, backward: forward } # Call a function. Return true if the selection changed. selectionChanged: (func) -> r = @selection.getRangeAt(0).cloneRange() func() rr = @selection.getRangeAt(0) - not (r.compareBoundaryPoints(Range.END_TO_END, rr) and r.compareBoundaryPoints Range.START_TO_START, rr) + not (r.compareBoundaryPoints(Range.END_TO_END, rr) or r.compareBoundaryPoints Range.START_TO_START, rr) - # Try to move one character in "direction". Return 1, -1 or 0, indicating that the selection got bigger, or - # smaller, or is unchanged. + # Try to move one character in "direction". Return 1, -1 or 0, indicating whether the selection got bigger, + # or smaller, or is unchanged. moveInDirection: (direction) -> length = @selection.toString().length @selection.modify "extend", direction, character @selection.toString().length - length - # Get the direction of the selection, either forward or backward. - # FIXME(smblott). There has to be a better way! - # NOTE(smblott). There is. See here: https://dom.spec.whatwg.org/#interface-range. + # Get the direction of the selection. The selection is "forward" if the focus is at or after the anchor, + # and "backward" otherwise. + # NOTE(smblott). Could be better, see: https://dom.spec.whatwg.org/#interface-range. getDirection: -> - # Try to move the selection forward or backward, then check whether it got bigger or smaller (then restore - # it). + # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it). for type in [ forward, backward ] if success = @moveInDirection type @moveInDirection @opposite[type] return if 0 < success then type else @opposite[type] - nextCharacter: (direction) -> - if @moveInDirection direction - text = @selection.toString() - @moveInDirection @opposite[direction] - text.charAt if @getDirection() == forward then text.length - 1 else 0 + moveForwardWord: (direction) -> + # We use two forward words and one backword so that we end up at the end of the word if we are at the end + # of the text. Currently broken if the very-next characters is whitespace. + movements = [ "forward word", "forward word", "forward character", "backward character", "backward word" ] + @runMovements movements - moveByWord: (direction) -> - @runMovement "#{direction} word" unless /\s/.test @nextCharacter direction - while /\s/.test @nextCharacter direction - break unless @selectionChanged => - @runMovement "#{direction} character" + swapFocusAndAnchor: -> + direction = @getDirection() + length = @selection.toString().length + @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]() + @selection.modify "extend", @opposite[direction], character for [0...length] - # Run a movement command. + # Run a movement command. Return true if the selection changed, false otherwise. runMovement: (movement) -> - length = @selection.toString().length - @selection.modify @alterMethod, movement.split(" ")... - @alterMethod == "move" or @selection.toString().length != length + @selectionChanged => @selection.modify @alterMethod, movement.split(" ")... + # Run a sequence of movements; bail immediately on any failure to change the selection. runMovements: (movements) -> - console.log movements for movement in movements break unless @runMovement movement @@ -125,45 +119,62 @@ class Movement extends MaintainCount "{": "backward paragraph" "$": "forward lineboundary" "0": "backward lineboundary" + "w": -> @moveForwardWord() + "o": -> @swapFocusAndAnchor() "G": "forward documentboundary" - "g": "backward documentboundary" - "w": -> @moveByWord forward - "W": -> @moveByWord forward - - "o": -> - # Swap the anchor and focus. This is too slow if the selection is large. - direction = @getDirection() - length = @selection.toString().length - @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]() - @selection.modify "extend", @opposite[direction], character for [0...length] + "gg": "backward documentboundary" constructor: (options) -> + @movements = extend {}, @movements + @commands = {} @alterMethod = options.alterMethod || "extend" - super options - - @push - _name: "movement" + @keyQueue = "" + @yankedText = "" + super extend options, keypress: (event) => @alwaysContinueBubbling => unless event.metaKey or event.ctrlKey or event.altKey - keyChar = String.fromCharCode event.charCode - if @movements[keyChar] - @selection = window.getSelection() - @runCountPrefixTimes => - switch typeof @movements[keyChar] - when "string" - @runMovement @movements[keyChar] - when "function" - @movements[keyChar].call @ - @scrollIntoView() + @keyQueue += String.fromCharCode event.charCode + @keyQueue = @keyQueue.slice Math.max 0, @keyQueue.length - 3 + for keyChar in (@keyQueue[i..] for i in [0...@keyQueue.length]) + if @movements[keyChar] or @commands[keyChar] + @keyQueue = "" + if @commands[keyChar] + @commands[keyChar].call @ + else if @movements[keyChar] + @selection = window.getSelection() + @runCountPrefixTimes => + switch typeof @movements[keyChar] + when "string" then @runMovement @movements[keyChar] + when "function" then @movements[keyChar].call @ + @scrollIntoView() + break + + # Aliases. + @movements.B = @movements.b + @movements.W = @movements.w + + yank: (args = {}) -> + @yankedText = text = window.getSelection().toString() + @selection.deleteFromDocument() if args.deleteFromDocument + @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() + @yankedText + + yankLine: -> + for direction in [ forward, backward ] + @runMovement "#{direction} lineboundary" + @swapFocusAndAnchor() + @lastYankedLine = @yank() # Adapted from: http://roysharon.com/blog/37. # I have no idea how this works (smblott, 2015/1/22). - getLeadingElement: (selection) -> + # The intention is to find the element containing the focus. That's the element we need to scroll into + # view. + getElementWithFocus: (selection) -> r = t = selection.getRangeAt 0 if selection.type == "Range" r = t.cloneRange() - r.collapse @getDirection() == backward + r.collapse(@getDirection() == backward) t = r.startContainer t = t.childNodes[r.startOffset] if t.nodeType == 1 o = t @@ -171,7 +182,7 @@ class Movement extends MaintainCount t = o || t?.parentNode t - # Try to scroll the leading end of the selection into view. + # Try to scroll the focus into view. scrollIntoView: -> if document.activeElement and DomUtils.isEditable document.activeElement element = document.activeElement @@ -182,106 +193,114 @@ class Movement extends MaintainCount coords = DomUtils.getCaretCoordinates element, element.selectionStart Scroller.scrollToPosition element, coords.top, coords.left else - # getLeadingElement() seems to work most, but not all, of the time. - leadingElement = @getLeadingElement @selection + # 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() - type = @selection.type - - if type == "None" - HUD.showForDuration "An initial selection is required for visual mode.", 2500 - return - # Try to start with a visible selection. - if type == "Caret" # or @selection.isCollapsed (broken if selection is in and input) - @moveInDirection(forward) or @moveInDirection backward + switch @selection.type + when "None" + HUD.showForDuration "An initial selection is required for visual mode.", 2500 + return + when "Caret" + # Try to start with a visible selection. + @moveInDirection(forward) or @moveInDirection backward unless options.underEditMode defaults = name: "visual" badge: "V" exitOnEscape: true alterMethod: "extend" + underEditMode: false + super extend defaults, options - keypress: (event) => - @alwaysContinueBubbling => - unless event.metaKey or event.ctrlKey or event.altKey - switch String.fromCharCode event.charCode - when "y" - text = window.getSelection().toString() - chrome.runtime.sendMessage - handler: "copyToClipboard" - data: text - @exit() - length = text.length - suffix = if length == 1 then "" else "s" - text = text[...12] + "..." if 15 < length - text = text.replace /\n/g, " " - HUD.showForDuration "Yanked #{length} character#{suffix}: \"#{text}\".", 2500 + extend @commands, + "y": @yank + "Y": @yankLine - super extend defaults, options - @debug = true - - # FIXME(smblott). We can't handle the selection changing with the mouse while while visual-mode is - # active. This "fix" doesn't work. - # work. - # onMouseUp = (event) => - # @alwaysContinueBubbling => - # if event.which == 1 - # window.removeEventListener onMouseUp - # new VisualMode @options - # window.addEventListener "mouseup", onMouseUp, true - - exit: -> - super() + if @options.underEditMode + extend @commands, + "d": => @yank deleteFromDocument: true + + yank: (args...) -> + text = super args... unless @options.underEditMode - if document.activeElement and DomUtils. isEditable document.activeElement + 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() + if @options.underEditMode + direction = @getDirection() + @selection[if direction == backward then "collapseToEnd" else "collapseToStart"]() + else + if document.activeElement and DomUtils.isEditable document.activeElement document.activeElement.blur() + # Now we set the clipboard. No operations which maniplulate the selection should follow this. + console.log "yank:", @yankedText.length, @yankedText + chrome.runtime.sendMessage { handler: "copyToClipboard", data: @yankedText } if @yankedText -class EditMode extends Movement - @activeElements = [] +class EditMode extends Movement constructor: (options = {}) -> - defaults = + @element = document.activeElement + return unless @element and DomUtils.isEditable @element + + super name: "edit" badge: "E" exitOnEscape: true alterMethod: "move" - @debug = true - @element = document.activeElement - return unless @element and DomUtils.isEditable @element - super extend defaults, options - handlerStack.debug = true - - extend @movements, - "i": => @enterInsertMode() - "a": => @enterInsertMode() + extend @commands, + "i": @enterInsertMode + "a": @enterInsertMode + "A": => @runMovement "forward lineboundary"; @enterInsertMode() "o": => @openLine forward "O": => @openLine backward - - exit: (event, target) -> - super() - @element.blur() if target? and DomUtils.isDOMDescendant @element, target - EditMode.activeElements = EditMode.activeElements.filter (element) => element != @element - - enterInsertMode: -> - new InsertMode - badge: "I" - blurOnEscape: false + "p": => @pasteClipboard forward + "P": => @pasteClipboard backward + "v": -> new VisualMode underEditMode: true + "yy": => @withRangeSelection => @yankLine() + + # Aliases. + @commands.Y = @commands.yy + + 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 openLine: (direction) -> @runMovement "#{direction} lineboundary" @enterInsertMode() - @simulateTextEntry "\n" + DomUtils.simulateTextEntry @element, "\n" @runMovement "backward character" if direction == backward - simulateTextEntry: (text) -> - event = document.createEvent "TextEvent" - event.initTextEvent "textInput", true, true, null, text - document.activeElement.dispatchEvent event + enterInsertMode: -> + new InsertMode { badge: "I", blurOnEscape: false } + + 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() root = exports ? window root.VisualMode = VisualMode diff --git a/lib/clipboard.coffee b/lib/clipboard.coffee index 2b28df70..836b57e4 100644 --- a/lib/clipboard.coffee +++ b/lib/clipboard.coffee @@ -15,13 +15,15 @@ Clipboard = document.execCommand("Copy") document.body.removeChild(textArea) - paste: -> + paste: (refocusElement = null) -> 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 diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index 477abef2..c1ce051f 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -231,6 +231,11 @@ DomUtils = @remove() false + simulateTextEntry: (element, text) -> + event = document.createEvent "TextEvent" + event.initTextEvent "textInput", true, true, null, text + element.dispatchEvent event + extend DomUtils, # From: https://github.com/component/textarea-caret-position/blob/master/index.js getCaretCoordinates: do -> |
