diff options
| -rw-r--r-- | background_scripts/commands.coffee | 3 | ||||
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 256 | ||||
| -rw-r--r-- | content_scripts/scroller.coffee | 2 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 3 | 
4 files changed, 125 insertions, 139 deletions
| diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index 67c4b9ad..80f18409 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -112,6 +112,7 @@ Commands =        "goToRoot",        "enterInsertMode",        "enterVisualMode", +      "enterVisualLineMode",        "enterEditMode",        "focusInput",        "LinkHints.activateMode", @@ -198,6 +199,7 @@ defaultKeyMappings =    "i": "enterInsertMode"    "v": "enterVisualMode" +  "V": "enterVisualLineMode"    "e": "enterEditMode"    "H": "goBack" @@ -288,6 +290,7 @@ commandDescriptions =    enterInsertMode: ["Enter insert mode", { noRepeat: true }]    enterVisualMode: ["Enter visual mode (not yet implemented)", { noRepeat: true }] +  enterVisualLineMode: ["Enter visual line mode (not yet implemented)", { noRepeat: true }]    enterEditMode: ["Enter vim-like edit mode (not yet implemented)", { noRepeat: true }]    focusInput: ["Focus the first text box on the page. Cycle between them using tab", diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index ad7fb59c..12d8bf4a 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,9 +1,8 @@  # To do: -# - edit-mode losing the focus -# - line-visual mode  # - better implementation of `o`  # - caret mode +# - find operations  # This prevents printable characters from being passed through to underlying page.  It should, however, allow  # through chrome keyboard shortcuts.  It's a backstop for all of the modes following. @@ -74,15 +73,9 @@ class Movement extends MaintainCount      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() -    length = @selection.toString().length -    func() -    rr = @selection.getRangeAt 0 -    rr.startContainer != r.startContainer or -      rr. startOffset != r.startOffset or -      @selection.toString().length != length +  # Run a movement command. +  runMovement: (movement) -> +    @selection.modify @alterMethod, movement.split(" ")...    # Try to move one character in "direction".  Return 1, -1 or 0, indicating whether the selection got bigger,    # or smaller, or is unchanged. @@ -96,64 +89,30 @@ class Movement extends MaintainCount    # NOTE(smblott). Could be better, see: https://dom.spec.whatwg.org/#interface-range.    getDirection: ->      # 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] +    for direction in [ forward, backward ] +      if success = @moveInDirection direction +        @moveInDirection @opposite[direction] +        return if 0 < success then direction else @opposite[direction] +    forward +  # An approximation of the vim "w" movement.    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 - -  swapFocusAndAnchor: -> +    # This is broken: +    # - On the very last word in the text. +    # - When the next character is not a word character. +    # However, it works well for the common cases, and the additional complexity of fixing these broken cases +    # is probably unwarranted right now (smblott, 2015/1/25). +    movements = [ "forward word", "forward word", "backward word" ] +    @runMovement movement for movement in movements + +  # Swap the focus and anchor. +  # FIXME(smblott). This implementation is rediculously inefficient if the selection is large. +  reverseSelection: ->      direction = @getDirection()      length = @selection.toString().length      @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 text -          @copy text -          locked = false -          result -      else -        func() - -  # Run a movement command.  Return true if the selection changed, false otherwise. -  runMovement: (movement) -> -    @selectionChanged => -      movement = movement.split(" ") -      if movement[0] == forward and movement[1] == "lineboundary" and @alterMethod == "extend" -        # Special case.  When we move forward to a line boundary, we're often just moving to the point at -        # which the text wraps, which is of no particular use.  Instead, we keep advancing until we find a -        # newline character.  This trailing newline character is included in the selection. -        atEndOfLine = => -          if @selectionChanged(=> @selection.modify @alterMethod, "forward", "character") -            text = @selection.toString() -            console.log text[text.length - 1] != "\n", text.length, text -            text[text.length - 1] == "\n" -          else -            true - -        @selection.modify @alterMethod, movement... -        @selection.modify @alterMethod, movement... while not atEndOfLine() -      else -        # Normal case. -        @selection.modify @alterMethod, movement... - -  # Run a sequence of movements; bail immediately on any failure to change the selection. -  runMovements: (movements) -> -    for movement in movements -      break unless @runMovement movement -    movements:      "l": "forward character"      "h": "backward character" @@ -161,7 +120,6 @@ class Movement extends MaintainCount      "k": "backward line"      "e": "forward word"      "b": "backward word" -    "w": -> @moveForwardWord()      ")": "forward sentence"      "(": "backward sentence"      "}": "forward paragraph" @@ -170,8 +128,9 @@ class Movement extends MaintainCount      "0": "backward lineboundary"      "G": "forward documentboundary"      "g": "backward documentboundary" +    "w": -> @moveForwardWord()      "Y": -> @selectLine() -    "o": -> @swapFocusAndAnchor() +    "o": -> @reverseSelection()    constructor: (options) ->      @selection = window.getSelection() @@ -180,13 +139,14 @@ class Movement extends MaintainCount      @alterMethod = options.alterMethod || "extend"      @keyQueue = ""      @yankedText = "" -    super extend options +    super options      # Aliases.      @movements.B = @movements.b      @movements.W = @movements.w      if @options.runMovement +      # This instance has been created just to run a single movement.        @handleMovementKeyChar @options.runMovement        @yank()        return @@ -198,22 +158,22 @@ class Movement extends MaintainCount            @keyQueue += String.fromCharCode event.charCode            # We allow at most three characters for a command or movement mapping.            @keyQueue = @keyQueue.slice Math.max 0, @keyQueue.length - 3 -          # Try each possible multi-character keyChar sequence, from longest to shortest. -          for keyChar in (@keyQueue[i..] for i in [0...@keyQueue.length]) -            if @movements[keyChar] or @commands[keyChar] -              @keyQueue = "" +          # Try each possible multi-character keyChar sequence, from longest to shortest (e.g. with "abc", we +          # try "abc", "bc" and "c"). +          for command in (@keyQueue[i..] for i in [0...@keyQueue.length]) +            if @movements[command] or @commands[command]                @selection = window.getSelection() +              @keyQueue = "" -              if @commands[keyChar] -                @commands[keyChar].call @ +              if @commands[command] +                @commands[command].call @                  @scrollIntoView()                  return @suppressEvent -              else if @movements[keyChar] -                @handleMovementKeyChar keyChar +              else if @movements[command] +                @handleMovementKeyChar command -                if @options.onYank or @options.oneMovementOnly -                  @scrollIntoView() +                if @options.oneMovementOnly                    @yank()                    return @suppressEvent @@ -222,7 +182,8 @@ class Movement extends MaintainCount          @continueBubbling    handleMovementKeyChar: (keyChar) -> -    # We need to copy the count prefix immediately, because protectClipboard is asynchronous. +    # We grab the count prefix immediately, because protectClipboard may be asynchronous (edit mode), and +    # @countPrefix may be reset if we wait.      count = if 0 < @countPrefix.length then parseInt @countPrefix else 1      @countPrefix = ""      if @movements[keyChar] @@ -233,42 +194,57 @@ class Movement extends MaintainCount              when "function" then @movements[keyChar].call @          @scrollIntoView() +  # Yank the selection.  Always exits.  Returns the yanked text.    yank: (args = {}) -> -    @yankedText = window.getSelection().toString() +    @yankedText = @selection.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 +    message = @yankedText.replace /\s+/g, " " +    length = message.length +    message = message[...12] + "..." if 15 < length +    plural = if length == 1 then "" else "s" +    HUD.showForDuration "Yanked #{length} character#{plural}: \"#{message}\".", 2500 +    @options.onYank.call @ @yankedText if @options.onYank      @exit()      @yankedText -  exit: (event) -> -    super() +  exit: (event, target) -> +    super event, target      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. +    unless event?.type == "keydown" and KeyboardUtils.isEscape event +      if 0 < @selection.toString().length +        @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]()      @copy @yankedText if @yankedText    selectLine: -> -    direction = @getDirection() -    for direction in [ @opposite[direction], direction ] -      console.log direction +    for direction in [ backward, forward ] +      @reverseSelection()        @runMovement "#{direction} lineboundary" -      @swapFocusAndAnchor() + +  # Try to scroll the focus into view. +  scrollIntoView: -> +    @protectClipboard => +      element = document.activeElement +      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 +        elementWithFocus = @getElementWithFocus @selection +        Scroller.scrollIntoView elementWithFocus if elementWithFocus    # Adapted from: http://roysharon.com/blog/37.    # I have no idea how this works (smblott, 2015/1/22).    # The intention is to find the element containing the focus.  That's the element we need to scroll into -  # view. +  # view. It seems to work most (but not all) of the time.    getElementWithFocus: (selection) ->      r = t = selection.getRangeAt 0      if selection.type == "Range" @@ -281,23 +257,6 @@ class Movement extends MaintainCount      t = o || t?.parentNode      t -  # Try to scroll the focus into view. -  scrollIntoView: -> -    @protectClipboard => -      element = document.activeElement -      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() @@ -314,7 +273,6 @@ class VisualMode extends Movement        badge: "V"        exitOnEscape: true        alterMethod: "extend" -      underEditMode: false      super extend defaults, options      extend @commands, @@ -325,12 +283,27 @@ class VisualMode extends Movement      if @options.underEditMode        extend @commands, -        "d": @yank +        "d": -> @yank deleteFromDocument: true          "c": -> @yank(); enterInsertMode() -class VisualModeForEdit extends VisualMode +    @clipboardContents = "" +    @paste (text) => @clipboardContents = text + +  protectClipboard: (func) -> +    func() +    @copy @clipboardContents + +  copy: (text) -> +    super @clipboardContents = text + +class VisualLineMode extends VisualMode    constructor: (options = {}) -> -    super extend options, underEditMode: true +    super options +    @selectLine() + +  handleMovementKeyChar: (keyChar) -> +    super keyChar +    @runMovement "#{@getDirection()} lineboundary", true  class EditMode extends Movement    constructor: (options = {}) -> @@ -351,40 +324,29 @@ class EditMode extends Movement        "O": => @openLine backward        "p": => @pasteClipboard forward        "P": => @pasteClipboard backward -      "v": -> new VisualModeForEdit +      "v": -> new VisualMode underEditMode: true -      "Y": -> @runInVisualMode runMovement: "Y" -      "y": => @runInVisualMode expectImmediateY: true -      "d": => @runInVisualMode deleteFromDocument: true -      "c": => @runInVisualMode +      "Y": -> @enterVisualMode runMovement: "Y" +      "y": => @enterVisualMode expectImmediateY: true +      "d": => @enterVisualMode deleteFromDocument: true +      "c": => @enterVisualMode          deleteFromDocument: true          onYank: -> new InsertMode { badge: "I", blurOnEscape: false } -      "D": => @runInVisualMode runMovement: "$", deleteFromDocument: true -      "C": => @runInVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode +      "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true +      "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode -    # # Aliases. -    # @commands.Y = @commands.yy - -  runInVisualMode: (options = {}) -> +  enterVisualMode: (options = {}) ->      defaults = +      underEditMode: true        initialCount: @countPrefix        oneMovementOnly: true -    new VisualModeForEdit extend defaults, options +    new VisualMode extend defaults, options      @countPrefix = ""    pasteClipboard: (direction) -> -    @protectClipboard (text) => -      if text -        # We use the heuristic that the paste is line oriented if the last character is a newline.  This is -        # consistent with the way runMovement selects text in visual mode for "forward lineboundary". -        lineOriented = /\n$/.test text -        if lineOriented -          @runMovement "#{direction} lineboundary" -          @runMovement "#{direction} character" if direction == forward -        DomUtils.simulateTextEntry @element, text -        # Slow!  Expensive!  Better way? -        @runMovement "backward character" for [0...text.length] if lineOriented +    @paste (text) => +      DomUtils.simulateTextEntry @element, text if text    openLine: (direction) ->      @runMovement "#{direction} lineboundary" @@ -394,13 +356,31 @@ class EditMode extends Movement    exit: (event, target) ->      super() -    if event?.type = "keydown" and KeyboardUtils.isEscape event +    if event?.type == "keydown" and KeyboardUtils.isEscape event        if target? and DomUtils.isDOMDescendant @element, target          @element.blur() +  # Backup the clipboard, then call a function (which may affect the selection text, and hence the +  # clipboard too), then restore the clipboard. +  protectClipboard: do -> +    locked = false +    clipboard = "" + +    (func) -> +      if locked +        func() +      else +        locked = true +        @paste (text) => +          clipboard = text +          func() +          @copy clipboard +          locked = false +  enterInsertMode = ->    new InsertMode { badge: "I", blurOnEscape: false }  root = exports ? window  root.VisualMode = VisualMode +root.VisualLineMode = VisualLineMode  root.EditMode = EditMode diff --git a/content_scripts/scroller.coffee b/content_scripts/scroller.coffee index f84dce8e..f26f0b73 100644 --- a/content_scripts/scroller.coffee +++ b/content_scripts/scroller.coffee @@ -246,7 +246,7 @@ Scroller =      amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]      CoreScroller.scroll element, direction, amount -  # FIXME(smblott). Implement scroll in the "x" dimension. +  # FIXME(smblott). We should also scroll in the "x" dimension.    scrollIntoView: (element) ->      activatedElement ||= document.body and firstScrollableElement()      rect = element.getBoundingClientRect() diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index fefb64ba..5fe40e5a 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -331,6 +331,9 @@ extend window,    enterVisualMode: ->      new VisualMode() +  enterVisualLineMode: -> +    new VisualLineMode +    enterEditMode: ->      @focusInput 1, EditMode | 
