diff options
| author | Stephen Blott | 2015-01-30 11:35:58 +0000 | 
|---|---|---|
| committer | Stephen Blott | 2015-01-30 13:52:37 +0000 | 
| commit | 6624740a639f3bb177a56999227bfcd24aba3712 (patch) | |
| tree | 56aa1e6e8661ab132c1467b2634779c1f850ce9b | |
| parent | ec4e2791537c57887471f0d7a6ab1d4e642d1744 (diff) | |
| download | vimium-6624740a639f3bb177a56999227bfcd24aba3712.tar.bz2 | |
Visual/edit modes: another minor code review.
| -rw-r--r-- | content_scripts/mode.coffee | 10 | ||||
| -rw-r--r-- | content_scripts/mode_visual_edit.coffee | 263 | ||||
| -rw-r--r-- | lib/utils.coffee | 6 | 
3 files changed, 160 insertions, 119 deletions
| diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee index a69c90a7..2d28d95b 100644 --- a/content_scripts/mode.coffee +++ b/content_scripts/mode.coffee @@ -151,11 +151,11 @@ class Mode    # case), because they do not need to be concerned with the value they yield.    alwaysContinueBubbling: handlerStack.alwaysContinueBubbling -  # Get a copy of the configuration options for this mode, but excluding the main keyboard-event handlers. -  getConfigurationOptions: -> -    options = extend {}, @options -    delete options[key] for key in [ "keydown", "keypress", "keyup" ] -    options +  # Activate a new instance of this mode, together with all of its original options (except its main +  # keybaord-event handlers; these will be recreated). +  cloneMode: -> +    delete @options[key] for key in [ "keydown", "keypress", "keyup" ] +    new @constructor @options    # Static method.  Used externally and internally to initiate bubbling of an updateBadge event and to send    # the resulting badge to the background page.  We only update the badge if this document (hence this frame) diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index f03c01ab..9b39c523 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -7,6 +7,9 @@  # Paste of whole lines.  # Arrow keys.  # J +# Fix Y for edit mode. +# Fix sentence movements. +# Change how we get the options for submodes.  # This prevents printable characters from being passed through to the underlying page.  It should, however,  # allow through Chrome keyboard shortcuts. @@ -56,6 +59,10 @@ class CountPrefix extends SuppressPrintable  forward = "forward"  backward = "backward"  character = "character" +word = "word" +line = "line" +sentence = "sentence" +paragraph = "paragraph"  vimword = "vimword"  lineboundary= "lineboundary" @@ -69,13 +76,16 @@ class Movement extends CountPrefix    copy: (text, isFinalUserCopy = false) ->      chrome.runtime.sendMessage handler: "copyToClipboard", data: text      # If isFinalUserCopy is set, then we're copying the final text selected by the user (and exiting). -    # However, we may be called from within @protectClipboard, which will later try to restore the clipboard's -    # contents.  Therefore, we disable copy so that subsequent calls will not be propagated. +    # However, we may be called again from within @protectClipboard, which will later try to restore the +    # original clipboard contents.  Therefore, we disable copy so that subsequent copies will not propagate.      @copy = (->) if isFinalUserCopy -  # This used whenever manipulating the selection may, as a side effect, change the clipboard's contents.  We -  # restore the original clipboard contents when we're done. May be asynchronous.  We use a lock so that calls -  # can be nested. +  # This s used whenever manipulating the selection may, as a side effect, change the clipboard's contents. +  # We restore the original clipboard contents when we're done. May be asynchronous.  We use a lock so that +  # calls can be nested. +  # +  # We do this primarily for edit mode, where the user does not expect caret movements to change the clipboard +  # contents.    protectClipboard: do ->      locked = false @@ -86,6 +96,7 @@ class Movement extends CountPrefix          @paste (text) =>            func(); @copy text; locked = false +  # Replace the current mode with another.  For example, replace visual mode with visual-line mode.    changeMode: (mode, options = {}) ->      @exit()      if @options.parentMode @@ -99,13 +110,16 @@ class Movement extends CountPrefix      if beforeText.length == 0 or @getDirection() == forward        @selection.modify "extend", forward, character        afterText = @selection.toString() -      @selection.modify "extend", backward, character unless beforeText == afterText -      afterText[afterText.length - 1] +      if beforeText != afterText +        @selection.modify "extend", backward, character +        afterText[afterText.length - 1]      else        beforeText[0] -  nextCharacterIsWordCharacter: -> -    /[A-Za-z0-9_]/.test @nextCharacter() +  # Test whether the character following the focus is a word character.  Leave the selection unchanged. +  nextCharacterIsWordCharacter: do -> +    regexp = /[A-Za-z0-9_]/ +    -> regexp.test @nextCharacter()    # Run a movement.  For convenience, the following three argument forms are available:    #   @runMovement "forward word" @@ -113,7 +127,7 @@ class Movement extends CountPrefix    #   @runMovement "forward", "word"    #    # The granularities are word, "line", "lineboundary", "sentence" and "paragraph".  In addition, we implement -  # the pseudo granularity "vimword", which implements vim-like word movement, for "w". +  # the pseudo granularity "vimword", which implements vim-like word movement (for "w").    #    runMovement: (args...) ->      # Normalize the various argument forms (to an array of two strings: direction and granularity). @@ -126,29 +140,24 @@ class Movement extends CountPrefix      # Perform the movement.      if movement[1] == vimword and movement[0] == forward        if @nextCharacterIsWordCharacter() -        @runMovements [ forward, "word" ], [ forward, vimword ] +        @runMovements [ forward, word ], [ forward, vimword ]        else -        @runMovements [ forward, "word" ], [ backward, "word" ] +        @runMovements [ forward, word ], [ backward, word ]      else if movement[1] == vimword -      @selection.modify @alterMethod, backward, "word" +      @selection.modify @alterMethod, backward, word      else        @selection.modify @alterMethod, movement... -  # Return a simple camparable value which depends on various aspects of the selection which may change when -  # the selection changes.  This is used to detect, after a movement, whether the selection has changed. +  # Return a simple camparable value which depends on various aspects of the selection.  This is used to +  # detect, after a movement, whether the selection has changed.    hashSelection: (debug) ->      range = @selection.getRangeAt(0) -    [ @element?.selectionStart -      @selection.toString().length -      range.anchorOffset -      range.focusOffset -      @selection.extentOffset -      @selection.baseOffset -    ].join "/" - -  # Call a function; return true if the selection changed. +    [ @element?.selectionStart, @selection.toString().length, range.anchorOffset, range.focusOffset, +      @selection.extentOffset, @selection.baseOffset ].join "/" + +  # Call a function; return true if the selection changed, false otherwise.    selectionChanged: (func) ->      before = @hashSelection(); func(); @hashSelection() != before @@ -158,22 +167,23 @@ class Movement extends CountPrefix        return false unless @selectionChanged => @runMovement movement      true -  # Swap the anchor node/offset and the focus node/offset (which implements "o" for visual mode). +  # Swap the anchor node/offset and the focus node/offset.  This allows us to work with both ends of the +  # selection, and implements "o" for visual mode.    reverseSelection: ->      element = document.activeElement      direction = @getDirection()      if element and DomUtils.isEditable(element) and not element.isContentEditable        # Note(smblott). This implementation is unacceptably expensive if the selection is large.  We only use -      # it when we have to.  However, the normal method (below) does not work for simple text inputs. +      # it here because the normal method (below) does not work for simple text inputs.        length = @selection.toString().length        @collapseSelectionToFocus()        @runMovement @opposite[direction], character for [0...length]      else -      # Normal method. +      # Normal method (efficient).        original = @selection.getRangeAt(0).cloneRange()        range = original.cloneRange()        range.collapse direction == backward -      @selectRange range +      @setSelectionRange range        which = if direction == forward then "start" else "end"        @selection.extend original["#{which}Container"], original["#{which}Offset"] @@ -186,7 +196,8 @@ class Movement extends CountPrefix    # 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. +  # NOTE(smblott). Could be better, see: https://dom.spec.whatwg.org/#interface-range (although that probably +  # won't work for text inputs).    getDirection: ->      # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it).      for direction in [ forward, backward ] @@ -203,12 +214,12 @@ class Movement extends CountPrefix      if 0 < @selection.toString().length        @selection[if @getDirection() == forward then "collapseToEnd" else "collapseToStart"]() -  selectRange: (range) -> +  setSelectionRange: (range) ->      @selection.removeAllRanges()      @selection.addRange range    # A movement can be a string (which will be passed to @runMovement() count times), or a function (which will -  # be called once with count as its argument. +  # be called once with count as its argument).    movements:      "l": "forward character"      "h": "backward character" @@ -225,13 +236,37 @@ class Movement extends CountPrefix      "$": "forward lineboundary"      "G": "forward documentboundary"      "gg": "backward documentboundary" +    "Y": (count) -> @selectLine count; @yank() + +  runMovementKeyChar: (args...) -> +    @protectClipboard => @handleMovementKeyChar args... + +  # Handle a single movement keyChar.  This is extended by super-classes. +  handleMovementKeyChar: (keyChar, count = 1) -> +    switch typeof @movements[keyChar] +      when "string" then @runMovement @movements[keyChar] for [0...count] +      when "function" then @movements[keyChar].call @, count +    @scrollIntoView() +  # Options for tweaking the behavior of movement mode and its sub-classes: +  #   - options.parentMode (a mode) +  #     This instance is a sub-mode of edit mode. +  #   - options.oneMovementOnly (truthy/falsy) +  #     This instance is created for one movement only, and then it yanks and exits. +  #   - options.immediateMovement (a string) +  #     This instance is created for one movement only, and this options specifies the movement (e.g. "j"). +  #   - options.deleteFromDocument (truthy/falsy) +  #     When yanking text, also delete it from the document. +  #   - options.onYank (a function) +  #     When yanking text, also call this function, passing the yanked text as an argument. +  #   - options.noCopyToClipboard (truthy/falsy) +  #     When yanking, do not copy the yanked text to the clipboard. +  #    constructor: (options) ->      @selection = window.getSelection()      @movements = extend {}, @movements      @commands = {}      @keyQueue = "" -    @keypressCount = 0      super options      # Aliases. @@ -239,16 +274,17 @@ class Movement extends CountPrefix      @movements.W = @movements.w      if @options.immediateMovement +      # This instance has been created to execute a single, given movement.        @runMovementKeyChar @options.immediateMovement, @getCountPrefix()        return +    # This is the main keyboard-event handler for movements and commands.      @push        _name: "#{@id}/keypress"        keypress: (event) => -        @keypressCount += 1          unless event.metaKey or event.ctrlKey or event.altKey            @keyQueue += String.fromCharCode event.charCode -          # Keep at most two characters in the key queue. +          # Keep at most two keyChars in the queue.            @keyQueue = @keyQueue.slice Math.max 0, @keyQueue.length - 2            for command in [ @keyQueue, @keyQueue[1..] ]              if command and (@movements[command] or @commands[command]) @@ -256,8 +292,8 @@ class Movement extends CountPrefix                @keyQueue = ""                # We need to treat "0" specially.  It can be either a movement, or a continutation of a count -              # prefix.  Don't treat it as a movement if we already have a count prefix. -              return @continueBubbling if command == "0" and 0 < @countPrefix +              # prefix.  Don't treat it as a movement if we already have an initial count prefix. +              return @continueBubbling if command == "0" and 0 < @countPrefix.length                if @commands[command]                  @commands[command].call @, @getCountPrefix() @@ -271,8 +307,8 @@ class Movement extends CountPrefix          @continueBubbling      # Install basic bindings for find mode, "n" and "N".  We do not install these bindings if this is a -    # sub-mode of edit mode, because we cannot (yet) guarantee that the selection will remain within the -    # active element. +    # sub-mode of edit mode (because we cannot guarantee that the selection will remain within the active +    # element), or if this instance has been created to execute only a single movement.      unless @options.parentMode or options.oneMovementOnly        do =>          executeFind = (count, findBackwards) => @@ -281,9 +317,12 @@ class Movement extends CountPrefix              for [0...count]                unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false                  HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 -                @selectRange initialRange +                @setSelectionRange initialRange                  @scrollIntoView() -                break +                return +            # The find was successfull. If we're in caret mode, then we should now have a selection, so we can +            # drop back into visual mode. +            @changeMode VisualMode if @name == "caret" and 0 < @selection.toString().length          @movements.n = (count) -> executeFind count, false          @movements.N = (count) -> executeFind count, true @@ -293,22 +332,11 @@ class Movement extends CountPrefix      #      # End of Movement constructor. -  runMovementKeyChar: (args...) -> -    @protectClipboard => @handleMovementKeyChar args... - -  handleMovementKeyChar: (keyChar, count = 1) -> -    switch typeof @movements[keyChar] -      when "string" -        @runMovement @movements[keyChar] for [0...count] -      when "function" -        @movements[keyChar].call @, count -    @scrollIntoView() -    # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text.    yank: (args = {}) ->      @yankedText = @selection.toString() -    if args.deleteFromDocument or @options.deleteFromDocument +    if @options.deleteFromDocument or args.deleteFromDocument        @selection.deleteFromDocument()      else        @collapseSelectionToAnchor() @@ -332,10 +360,10 @@ class Movement extends CountPrefix      # Move over count entities.      for [0...count]        return unless @runMovements [ forward, entity ] -      # Also consume the next character. For "lineboundary", this consumes the following newline, allowing us -      # to move on to the next line (for "3dd", "3yy", etc). -      @runMovement forward, character -    # Move to the start of the subsequent entity +      # For "lineboundary", we consume the following newline, allowing us to move on to the next line (for +      # "3dd", "3yy", etc). +      @runMovement forward, character if entity == lineboundary +    # Move to the start of the subsequent entity.      @runMovements [ forward, entity ], [ backward, entity ]    # Try to scroll the focus into view. @@ -344,7 +372,7 @@ class Movement extends CountPrefix        if @element and DomUtils.isEditable @element          if @element.clientHeight < @element.scrollHeight            if @element.isContentEditable -            # How do we do this?  This case matters for gmail and Google's inbox. +            # Help(smblott). How do we do this?  This case matters for gmail and Google's inbox.            else              position = if @getDirection() == backward then @element.selectionStart else @element.selectionEnd              coords = DomUtils.getCaretCoordinates @element, position @@ -365,20 +393,24 @@ class VisualMode extends Movement        exitOnEscape: true      super extend defaults, options -    switch @selection.type -      when "None" -        HUD.showForDuration "No selection, entering caret mode first.", 2500 -        return @changeMode CaretMode -      when "Caret" -        @selection.modify "extend", forward, character unless @options.oneMovementOnly +    if @options.parentMode and @selection.type == "Caret" +      # We're being called from edit mode, so establish an intial visible selection. +      @extendByOneCharacter(forward) or @extendByOneCharacter backward +    else if @selection.type in [ "None", "Caret" ] +      unless @options.oneMovementOnly or options.immediateMovement +        HUD.showForDuration "No selection, entering caret mode first..", 2500 +        @changeMode CaretMode +        return      # Yank on <Enter>.      @push        _name: "#{@id}/enter"        keypress: (event) => -        if event.keyCode == keyCodes.enter and not (event.metaKey or event.ctrlKey or event.altKey) -          @yank(); @suppressEvent -        else @continueBubbling +        if event.keyCode == keyCodes.enter +          unless event.metaKey or event.ctrlKey or event.altKey or event.shiftKey +            @yank() +            return @suppressEvent +        @continueBubbling      # Visual-mode commands.      unless @options.oneMovementOnly @@ -388,7 +420,6 @@ class VisualMode extends Movement        @commands.V = -> @changeMode VisualLineMode        @commands.c = -> @changeMode CaretMode        @commands.o = -> @reverseSelection() -      @commands.Y = (count) -> @selectLine count; @yank()        # Additional commands when run under edit mode.        if @options.parentMode @@ -399,20 +430,15 @@ class VisualMode extends Movement      # For edit mode's "yy" and "dd".      if @options.yankLineCharacter        @commands[@options.yankLineCharacter] = (count) -> -        if @keypressCount == 1 -          @selectLine count -          @yank() +        @selectLine count; @yank()      # For edit mode's "daw", "cas", and so on.      if @options.oneMovementOnly        @commands.a = (count) -> -        if @keypressCount == 1 -          for entity in [ "word", "sentence", "paragraph" ] -            do (entity) => -              @commands[entity.charAt 0] = -> -                if @keypressCount == 2 -                  @selectLexicalEntity entity, count -                  @yank() +        for entity in [ word, sentence, paragraph ] +          do (entity) => +            @commands[entity.charAt 0] = -> +              @selectLexicalEntity entity, count; @yank()      #      # End of VisualMode constructor. @@ -431,13 +457,13 @@ class VisualMode extends Movement    handleMovementKeyChar: (args...) ->      super args... -    @yank() if @options.oneMovementOnly +    @yank() if @options.oneMovementOnly or @options.immediateMovement    selectLine: (count) ->      @reverseSelection() if @getDirection() == forward      @runMovement backward, lineboundary      @reverseSelection() -    @runMovement forward, "line" for [1...count] +    @runMovement forward, line for [1...count]      @runMovement forward, lineboundary      @runMovement forward, character @@ -492,7 +518,7 @@ class CaretMode extends Movement    # When visual mode starts and there's no existing selection, we launch CaretMode and try to establish a    # selection.  As a heuristic, we pick the first non-whitespace character of the first visible text node -  # which seems to be long enough to be interesting. +  # which seems to be big enough to be interesting.    establishInitialSelectionAnchor: ->      nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT      while node = nodes.nextNode() @@ -500,11 +526,12 @@ class CaretMode extends Movement        if node.nodeType == 3 and 50 <= node.data.trim().length          element = node.parentElement          if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element +          # Start at the offset of the first non-whitespace character.            offset = node.data.length - node.data.replace(/^\s+/, "").length            range = document.createRange()            range.setStart node, offset            range.setEnd node, offset -          @selectRange range +          @setSelectionRange range            return true      false @@ -524,8 +551,8 @@ class EditMode extends Movement      extend @commands,        i: -> @enterInsertMode()        a: -> @enterInsertMode() -      I: -> @runMovement "backward lineboundary"; @enterInsertMode() -      A: -> @runMovement "forward lineboundary"; @enterInsertMode() +      I: -> @runMovement backward, lineboundary; @enterInsertMode() +      A: -> @runMovement forward, lineboundary; @enterInsertMode()        o: -> @openLine forward        O: -> @openLine backward        p: -> @pasteClipboard forward @@ -533,6 +560,7 @@ class EditMode extends Movement        v: -> @launchSubMode VisualMode        V: -> @launchSubMode VisualLineMode +      # FIXME(smblott).  "Y" is no longer a movement, it's a command.  This needs to be implemented.        Y: (count) -> @enterVisualModeForMovement count, immediateMovement: "Y"        x: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true, noCopyToClipboard: true        X: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true, noCopyToClipboard: true @@ -570,7 +598,10 @@ class EditMode extends Movement      #   if @element.value.trim() == ""      #     @enterInsertMode()      #     HUD.showForDuration "Input empty, entered insert mode directly.", 3500 +    # +    # End of edit-mode constructor. +  # For "~", "3~", "g~3w", "g~e", and so on.    swapCase: (count, immediate) ->      @enterVisualModeForMovement count,        immediateMovement: if immediate then "l" else null @@ -585,6 +616,29 @@ class EditMode extends Movement                char.toLowerCase()          DomUtils.simulateTextEntry @element, chars.join "" +  # For "p" and "P". +  pasteClipboard: (direction) -> +    @paste (text) => +      if text +        # We use the following heuristic: if the text ends with a newline character, then it's a line-oriented +        # paste, and should be pasted in at a line break. +        if /\n$/.test text +          @runMovement backward, lineboundary +          @runMovement forward, line if direction == forward +          DomUtils.simulateTextEntry @element, text +          @runMovement backward, line +        else +          DomUtils.simulateTextEntry @element, text + +  # For "o" and "O". +  openLine: (direction) -> +    @runMovement direction, lineboundary +    DomUtils.simulateTextEntry @element, "\n" +    @runMovement backward, character if direction == backward +    @enterInsertMode() + +  # This lanches a visual-mode instance for one movement only, (usually) yanks the resulting selected text, +  # and (possibly) deletes it.    enterVisualModeForMovement: (count, options = {}) ->      @launchSubMode VisualMode, extend options,        badge: "M" @@ -600,28 +654,10 @@ class EditMode extends Movement      @activeSubMode?.instance.exit()      @activeSubMode =        mode: mode +      options: options        instance: new mode extend options, parentMode: @      @activeSubMode.instance.onExit => @activeSubMode = null -  pasteClipboard: (direction) -> -    @paste (text) => -      if text -        # We use the following heuristic: if the text ends in a newline character, then it's a line-oriented -        # paste, and should be pasted in at a line break. -        if /\n$/.test text -          @runMovement backward, lineboundary -          @runMovement forward, "line" if direction == forward -          DomUtils.simulateTextEntry @element, text -          @runMovement backward, "line" -        else -          DomUtils.simulateTextEntry @element, text - -  openLine: (direction) -> -    @runMovement direction, lineboundary -    DomUtils.simulateTextEntry @element, "\n" -    @runMovement backward, character if direction == backward -    @enterInsertMode() -    exit: (event, target) ->      super event, target @@ -635,16 +671,16 @@ class EditMode extends Movement      if event?.type == "blur"        # This instance of edit mode has now been entirely removed from the handler stack.  It is inactive. -      # However, the user may return.  For example, we get a blur event when we change tab.  Or, the user may -      # be copying text with the mouse.   When the user does return, they expect to still be in edit mode.  We -      # leave behind a "suspended-edit" mode which watches for focus events and activates a new edit-mode -      # instance if required. +      # However, the user hasn't asked to leave edit mode, and may return.  For example, we get a blur event +      # when we change tab.  Or, the user may be copying text with the mouse.   When the user does return, +      # they expect to still be in edit mode.  We leave behind a "suspended-edit" mode which watches for focus +      # events and activates a new edit-mode instance if required.        # -      # How this gets cleaned up is a bit tricky.  The suspended-edit mode remains active on the current input -      # element indefinitely.  However, the only way to enter edit mode is via focusInput.  And all modes -      # launched by focusInput on a particular input element share a singleton (the element itself).  In -      # addition, the new mode below shares the same singleton.  So any new insert-mode or edit-mode instance -      # on this target element (the singleton) displaces any previously-active mode (including any +      # How does this get cleaned up?  It's a bit tricky.  The suspended-edit mode remains active on the +      # current input element indefinitely.  However, the only way to enter edit mode is via focusInput.  And +      # all modes launched by focusInput on a particular input element share a singleton (the element itself). +      # In addition, the new mode below shares the same singleton.  So any new insert-mode or edit-mode +      # instance on this target element (the singleton) displaces any previously-active mode (including any        # suspended-edit mode).  PostFindMode shares the same singleton.        #        (new Mode name: "#{@id}-suspended", singleton: @options.singleton).push @@ -652,9 +688,8 @@ class EditMode extends Movement          focus: (event) =>            @alwaysContinueBubbling =>              if event?.target == @options.targetElement -              editMode = new EditMode @getConfigurationOptions() -              if activeSubMode -                editMode.launchSubMode activeSubMode.mode, activeSubMode.instance.getConfigurationOptions() +              editMode = new EditMode Utils.copyObjectOmittingProperties @options, "keydown", "keypress", "keyup" +              editMode.launchSubMode activeSubMode.mode, activeSubMode.options if activeSubMode  root = exports ? window  root.VisualMode = VisualMode diff --git a/lib/utils.coffee b/lib/utils.coffee index c04bf417..64c87842 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -163,6 +163,12 @@ Utils =          identities.push obj        "identity-" + index +  # Return a copy of object, but with some of its properties omitted. +  copyObjectOmittingProperties: (obj, properties...) -> +    obj = extend {}, obj +    delete obj[property] for property in properties +    obj +  # This creates a new function out of an existing function, where the new function takes fewer arguments. This  # allows us to pass around functions instead of functions + a partial list of arguments.  Function::curry = -> | 
