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 -> | 
