From bf5797207fffdb47714d91282d5ce314314e65f6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 05:08:39 +0000 Subject: Visual/edit modes: consolidate both modes in a single file. These modes turn out to be almost identical. So they're better off in a single file. --- content_scripts/mode_visual_edit.coffee | 133 ++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 content_scripts/mode_visual_edit.coffee (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee new file mode 100644 index 00000000..3a8f5839 --- /dev/null +++ b/content_scripts/mode_visual_edit.coffee @@ -0,0 +1,133 @@ + +# This prevents unmapped printable characters from being passed through to underlying page. +class SuppressPrintable extends Mode + constructor: (options) -> + + handler = (event) => + if KeyboardUtils.isPrintable event + if event.type == "keydown" + DomUtils. suppressPropagation + @stopBubblingAndTrue + else + @suppressEvent + else + @stopBubblingAndTrue + + @suppressPrintableHandlerId = handlerStack.push + _name: "movement/suppress-printable" + keydown: handler + keypress: handler + keyup: handler + + super options + @onExit => handlerStack.remove @suppressPrintableHandlerId + +# This watches keyboard events, and maintains @countPrefix as count-prefic and other keys are pressed. +class MaintainCount extends SuppressPrintable + constructor: (options) -> + @countPrefix = "" + super options + + isNumberKey = (keyChar) -> + keyChar and keyChar.length == 1 and "0" <= keyChar <= "9" + + @push + _name: "movement/maintain-count" + keypress: (event) => + @alwaysContinueBubbling => + unless event.metaKey or event.ctrlKey or event.altKey + keyChar = String.fromCharCode event.charCode + @countPrefix = if isNumberKey keyChar then @countPrefix + keyChar else "" + + countPrefixTimes: (func) -> + countPrefix = if 0 < @countPrefix.length then parseInt @countPrefix else 1 + @countPrefix = "" + func() for [0...countPrefix] + +# This implements movement commands with count prefixes (using MaintainCount) for visual and edit modes. +class Movement extends MaintainCount + movements: + h: "backward character" + l: "forward character" + k: "backward line" + j: "forward line" + b: "backward word" + e: "forward word" + + constructor: (options) -> + @alterMethod = options.alterMethod || "extend" + super options + + @push + _name: "movement" + keypress: (event) => + @alwaysContinueBubbling => + unless event.metaKey or event.ctrlKey or event.altKey + keyChar = String.fromCharCode event.charCode + if @movements[keyChar] + @countPrefixTimes => + if "string" == typeof @movements[keyChar] + window.getSelection().modify @alterMethod, @movements[keyChar].split(/\s+/)... + else if "function" == typeof @movements[keyChar] + @movements[keyChar]() + +class VisualMode extends Movement + constructor: (options = {}) -> + defaults = + name: "visual" + badge: "V" + exitOnEscape: true + exitOnBlur: options.targetElement + alterMethod: "extend" + + keypress: (event) => + @alwaysContinueBubbling => + unless event.metaKey or event.ctrlKey or event.altKey + switch String.fromCharCode event.charCode + when "y" + chrome.runtime.sendMessage + handler: "copyToClipboard" + data: window.getSelection().toString() + @exit() + # TODO(smblott). Suppress next keyup. + + super extend defaults, options + @debug = true + +class EditMode extends Movement + @activeElements = [] + + constructor: (options = {}) -> + defaults = + name: "edit" + exitOnEscape: true + alterMethod: "move" + keydown: (event) => if @isActive() then @handleKeydown event else @continueBubbling + keypress: (event) => if @isActive() then @handleKeypress event else @continueBubbling + keyup: (event) => if @isActive() then @handleKeyup event else @continueBubbling + + @element = document.activeElement + if @element and DomUtils.isEditable @element + super extend defaults, options + + handleKeydown: (event) -> + @stopBubblingAndTrue + handleKeypress: (event) -> + @suppressEvent + handleKeyup: (event) -> + @stopBubblingAndTrue + + isActive: -> + document.activeElement and DomUtils.isDOMDescendant @element, document.activeElement + + exit: (event, target) -> + super() + @element.blur() if target? and DomUtils.isDOMDescendant @element, target + EditMode.activeElements = EditMode.activeElements.filter (element) => element != @element + + updateBadge: (badge) -> + badge.badge = "E" if @isActive() + +root = exports ? window +root.VisualMode = VisualMode +root.EditMode = EditMode -- cgit v1.2.3 From 92ccb9cbf5fd9fc3c21cd7c7be8052c55a8360b6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 06:42:07 +0000 Subject: Visual/edit modes: continued development... - Some better comments. - More commands. - The "o" command for visual mode (the implementation is poor, there has to be a better way). --- content_scripts/mode_visual_edit.coffee | 92 ++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 23 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 3a8f5839..ec244895 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,18 +1,21 @@ -# This prevents unmapped printable characters from being passed through to underlying page. +# 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. class SuppressPrintable extends Mode constructor: (options) -> handler = (event) => if KeyboardUtils.isPrintable event if event.type == "keydown" - DomUtils. suppressPropagation + DomUtils.suppressPropagation @stopBubblingAndTrue else @suppressEvent else @stopBubblingAndTrue + # This is pushed onto the handler stack before calling super(). Therefore, it ends up underneath (or + # after) all of the other handlers associated with the mode. @suppressPrintableHandlerId = handlerStack.push _name: "movement/suppress-printable" keydown: handler @@ -22,38 +25,80 @@ class SuppressPrintable extends Mode super options @onExit => handlerStack.remove @suppressPrintableHandlerId -# This watches keyboard events, and maintains @countPrefix as count-prefic and other keys are pressed. +# This watches keyboard events and maintains @countPrefix as count and other keys are pressed. class MaintainCount extends SuppressPrintable constructor: (options) -> @countPrefix = "" super options - isNumberKey = (keyChar) -> - keyChar and keyChar.length == 1 and "0" <= keyChar <= "9" - @push _name: "movement/maintain-count" keypress: (event) => @alwaysContinueBubbling => unless event.metaKey or event.ctrlKey or event.altKey keyChar = String.fromCharCode event.charCode - @countPrefix = if isNumberKey keyChar then @countPrefix + keyChar else "" + @countPrefix = + if keyChar and keyChar.length == 1 and "0" <= keyChar <= "9" + @countPrefix + keyChar + else + "" - countPrefixTimes: (func) -> - countPrefix = if 0 < @countPrefix.length then parseInt @countPrefix else 1 - @countPrefix = "" - func() for [0...countPrefix] + runCountPrefixTimes: (func) -> + count = if 0 < @countPrefix.length then parseInt @countPrefix else 1 + func() for [0...count] # This implements movement commands with count prefixes (using MaintainCount) for visual and edit modes. class Movement extends MaintainCount - movements: - h: "backward character" - l: "forward character" - k: "backward line" - j: "forward line" - b: "backward word" - e: "forward word" + # Get the direction of the selection, either "forward" or "backward". + # FIXME(smblott). There has to be a better way! + getDirection: (selection) -> + length = selection.toString().length + # Try to move the selection forward, then check whether it got bigger or smaller (then restore it). + selection.modify "extend", "forward", "character" + if length != selection.toString().length + direction = if selection.toString().length < length then "backward" else "forward" + selection.modify "extend", "backward", "character" + direction + else + # If we can't move forward, we could be at the end of the document, so try moving backward instead. + selection.modify "extend", "backward", "character" + if length != selection.toString().length + direction = if selection.toString().length < length then "forward" else "backward" + selection.modify "extend", "forward", "character" + direction + else + # Surely one of those has to work. What now? + "unknown" + + movements: + "l": "forward character" + "h": "backward character" + "j": "forward line" + "k": "backward line" + "e": "forward word" + "b": "backward word" + ")": "forward sentence" + "(": "backward sentence" + "}": "forward paragraph" + "{": "backward paragraph" + "$": "forward lineboundary" + "0": "backward lineboundary" + "G": "forward documentboundary" + "g": "backward documentboundary" + + "o": -> + selection = window.getSelection() + length = selection.toString().length + switch @getDirection selection + when "forward" + selection.collapseToEnd() + selection.modify "extend", "backward", "character" for [0...length] + when "backward" + selection.collapseToStart() + selection.modify "extend", "forward", "character" for [0...length] + + # TODO(smblott). What do we do if there is no initial selection? Or multiple ranges? constructor: (options) -> @alterMethod = options.alterMethod || "extend" super options @@ -65,11 +110,12 @@ class Movement extends MaintainCount unless event.metaKey or event.ctrlKey or event.altKey keyChar = String.fromCharCode event.charCode if @movements[keyChar] - @countPrefixTimes => - if "string" == typeof @movements[keyChar] - window.getSelection().modify @alterMethod, @movements[keyChar].split(/\s+/)... - else if "function" == typeof @movements[keyChar] - @movements[keyChar]() + @runCountPrefixTimes => + switch typeof @movements[keyChar] + when "string" + window.getSelection().modify @alterMethod, @movements[keyChar].split(" ")... + when "function" + @movements[keyChar].call @ class VisualMode extends Movement constructor: (options = {}) -> -- cgit v1.2.3 From dd9b2e9550a48c8e6e7a4a56b530ac8060279b12 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 08:49:45 +0000 Subject: Visual/edit modes: initial "w" and "W" commands. --- content_scripts/mode_visual_edit.coffee | 73 +++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 18 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index ec244895..cc0f0bf6 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -50,26 +50,60 @@ class MaintainCount extends SuppressPrintable # This implements movement commands with count prefixes (using MaintainCount) for visual and edit modes. class Movement extends MaintainCount + other: + forward: "backward" + backward: "forward" + + # Try to move one character in "direction". Return 1, -1 or 0, indicating that the selection got bigger or + # smaller, or is unchanged. + moveInDirection: (direction, selection = window.getSelection()) -> + 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! - getDirection: (selection) -> - length = selection.toString().length + getDirection: (selection = window.getSelection()) -> # Try to move the selection forward, then check whether it got bigger or smaller (then restore it). - selection.modify "extend", "forward", "character" - if length != selection.toString().length - direction = if selection.toString().length < length then "backward" else "forward" - selection.modify "extend", "backward", "character" - direction - else - # If we can't move forward, we could be at the end of the document, so try moving backward instead. - selection.modify "extend", "backward", "character" - if length != selection.toString().length - direction = if selection.toString().length < length then "forward" else "backward" - selection.modify "extend", "forward", "character" - direction - else - # Surely one of those has to work. What now? - "unknown" + success = @moveInDirection "forward", selection + if success + @moveInDirection "backward", selection + return if success < 0 then "backward" else "forward" + + # If we can't move forward, we could be at the end of the document, so try moving backward instead. + success = @moveInDirection "backward", selection + if success + @moveInDirection "forward", selection + return if success < 0 then "forward" else "backward" + + "none" + + nextCharacter: (direction) -> + if @moveInDirection direction + text = window.getSelection().toString() + @moveInDirection @other[direction] + console.log text.charAt(if direction == "forward" then text.length - 1 else 0) + text.charAt(if @getDirection() == "forward" then text.length - 1 else 0) + + moveByWord: (direction) -> + # We go to the end of the next word, then come back to the start of it. + movements = [ "#{direction} word", "#{@other[direction]} word" ] + # If we're in the middle of a word, then we need to first skip over it. + console.log @nextCharacter direction + switch direction + when "forward" + movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction + when "backward" + movements.push "#{direction} word" unless /\s/.test @nextCharacter direction + console.log movements + @runMovements movements + + runMovement: (movement) -> + window.getSelection().modify @alterMethod, movement.split(" ")... + + runMovements: (movements) -> + for movement in movements + @runMovement movement movements: "l": "forward character" @@ -87,6 +121,9 @@ class Movement extends MaintainCount "G": "forward documentboundary" "g": "backward documentboundary" + "w": -> @moveByWord "forward" + "W": -> @moveByWord "backward" + "o": -> selection = window.getSelection() length = selection.toString().length @@ -113,7 +150,7 @@ class Movement extends MaintainCount @runCountPrefixTimes => switch typeof @movements[keyChar] when "string" - window.getSelection().modify @alterMethod, @movements[keyChar].split(" ")... + @runMovement @movements[keyChar] when "function" @movements[keyChar].call @ -- cgit v1.2.3 From d7b416747e5cf6971737a3f70243618419a1ac4b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 11:39:21 +0000 Subject: Visual/edit modes: further development. - Better abstraction. - Add HUD message on yank. - Require initial selection for visual mode. - Try to start with a visible selection. - Scroll the active end of the selection into view (with smooth scrolling, if enabled). --- content_scripts/mode_visual_edit.coffee | 153 ++++++++++++++++++++------------ 1 file changed, 98 insertions(+), 55 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index cc0f0bf6..a5d8ff45 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -8,9 +8,9 @@ class SuppressPrintable extends Mode if KeyboardUtils.isPrintable event if event.type == "keydown" DomUtils.suppressPropagation - @stopBubblingAndTrue + @stopBubblingAndFalse else - @suppressEvent + false else @stopBubblingAndTrue @@ -25,7 +25,7 @@ class SuppressPrintable extends Mode super options @onExit => handlerStack.remove @suppressPrintableHandlerId -# This watches keyboard events and maintains @countPrefix as count and other keys are pressed. +# This watches keyboard events and maintains @countPrefix as number and other keys are pressed. class MaintainCount extends SuppressPrintable constructor: (options) -> @countPrefix = "" @@ -47,63 +47,57 @@ class MaintainCount extends SuppressPrintable count = if 0 < @countPrefix.length then parseInt @countPrefix else 1 func() for [0...count] -# This implements movement commands with count prefixes (using MaintainCount) for visual and edit modes. +forward = "forward" +backward = "backward" +character = "character" + +# This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit +# mode. class Movement extends MaintainCount - other: - forward: "backward" - backward: "forward" + opposite: + forward: backward + backward: forward - # Try to move one character in "direction". Return 1, -1 or 0, indicating that the selection got bigger or + # Try to move one character in "direction". Return 1, -1 or 0, indicating that the selection got bigger, or # smaller, or is unchanged. - moveInDirection: (direction, selection = window.getSelection()) -> - length = selection.toString().length - selection.modify "extend", direction, "character" - selection.toString().length - length + 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". + # Get the direction of the selection, either forward or backward. # FIXME(smblott). There has to be a better way! - getDirection: (selection = window.getSelection()) -> - # Try to move the selection forward, then check whether it got bigger or smaller (then restore it). - success = @moveInDirection "forward", selection - if success - @moveInDirection "backward", selection - return if success < 0 then "backward" else "forward" - - # If we can't move forward, we could be at the end of the document, so try moving backward instead. - success = @moveInDirection "backward", selection - if success - @moveInDirection "forward", selection - return if success < 0 then "forward" else "backward" - - "none" + getDirection: -> + # Try to move the selection forward or backward, then 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 = window.getSelection().toString() - @moveInDirection @other[direction] - console.log text.charAt(if direction == "forward" then text.length - 1 else 0) - text.charAt(if @getDirection() == "forward" then text.length - 1 else 0) + text = @selection.toString() + @moveInDirection @opposite[direction] + text.charAt if @getDirection() == forward then text.length - 1 else 0 moveByWord: (direction) -> # We go to the end of the next word, then come back to the start of it. - movements = [ "#{direction} word", "#{@other[direction]} word" ] - # If we're in the middle of a word, then we need to first skip over it. - console.log @nextCharacter direction - switch direction - when "forward" - movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction - when "backward" - movements.push "#{direction} word" unless /\s/.test @nextCharacter direction - console.log movements + movements = [ "#{direction} word", "#{@opposite[direction]} word" ] + # If we're in the middle of a word, then we also need to skip over that one. + movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction @runMovements movements + # Run a movement command. Return true if the length of the selection changed, false otherwise. runMovement: (movement) -> - window.getSelection().modify @alterMethod, movement.split(" ")... + length = @selection.toString().length + @selection.modify @alterMethod, movement.split(" ")... + @selection.toString().length != length runMovements: (movements) -> for movement in movements - @runMovement movement + break unless @runMovement movement movements: "l": "forward character" @@ -121,19 +115,19 @@ class Movement extends MaintainCount "G": "forward documentboundary" "g": "backward documentboundary" - "w": -> @moveByWord "forward" - "W": -> @moveByWord "backward" + "w": -> @moveByWord forward + "W": -> @moveByWord backward "o": -> - selection = window.getSelection() - length = selection.toString().length - switch @getDirection selection - when "forward" - selection.collapseToEnd() - selection.modify "extend", "backward", "character" for [0...length] - when "backward" - selection.collapseToStart() - selection.modify "extend", "forward", "character" for [0...length] + # FIXME(smblott). This is super slow if the selection is large. + length = @selection.toString().length + switch @getDirection() + when forward + @selection.collapseToEnd() + @selection.modify "extend", backward, character for [0...length] + when backward + @selection.collapseToStart() + @selection.modify "extend", forward, character for [0...length] # TODO(smblott). What do we do if there is no initial selection? Or multiple ranges? constructor: (options) -> @@ -147,15 +141,45 @@ class Movement extends MaintainCount 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 @ + # Try to scroll the leading end of the selection into view. getLeadingElement() seems to work + # most, but not all, of the time. + leadingElement = @getLeadingElement @selection + Scroller.scrollIntoView leadingElement if leadingElement + + # Adapted from: http://roysharon.com/blog/37. + # I have no idea how this works (smblott, 2015/1/22). + getLeadingElement: (selection) -> + r = t = selection.getRangeAt 0 + if selection.type == "Range" + r = t.cloneRange() + r.collapse @getDirection() == backward + t = r.startContainer + t = t.childNodes[r.startOffset] if t.nodeType == 1 + o = t + o = o.previousSibling while o and o.nodeType != 1 + t = o || t?.parentNode + t 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 + @moveInDirection(forward) or @moveInDirection backward + defaults = name: "visual" badge: "V" @@ -168,15 +192,34 @@ class VisualMode extends Movement 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: window.getSelection().toString() + data: text @exit() - # TODO(smblott). Suppress next keyup. + handlerStack.push keyup: => false + length = text.length + suffix = if length == 1 then "" else "s" + text = text[...12] + "..." if 15 < length + HUD.showForDuration "Yanked #{length} character#{suffix}: \"#{text}\".", 2500 super extend defaults, options @debug = true + # FIXME(smblott). + # onMouseUp = (event) => + # @alwaysContinueBubbling => + # if event.which == 1 + # window.removeEventListener onMouseUp + # new VisualMode @options + # window.addEventListener "mouseup", onMouseUp, true + + exit: -> + super() + unless @options.underEditMode + if document.activeElement and DomUtils. isEditable document.activeElement + document.activeElement.blur() + class EditMode extends Movement @activeElements = [] -- cgit v1.2.3 From 5493c2f3f878f495a41eb524ab1a8030c7411796 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 15:10:50 +0000 Subject: Visual/edit modes: faster "o" implementation. --- content_scripts/mode_visual_edit.coffee | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index a5d8ff45..0f335a9b 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -68,6 +68,7 @@ class Movement extends MaintainCount # 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. getDirection: -> # Try to move the selection forward or backward, then check whether it got bigger or smaller (then restore # it). @@ -119,15 +120,23 @@ class Movement extends MaintainCount "W": -> @moveByWord backward "o": -> - # FIXME(smblott). This is super slow if the selection is large. - length = @selection.toString().length + # Swap the anchor and focus. + # Note(smblott). I can't find an approach which works for both cases, so we have to implement each case + # separately. + original = @selection.getRangeAt 0 switch @getDirection() when forward - @selection.collapseToEnd() - @selection.modify "extend", backward, character for [0...length] + range = original.cloneRange() + range.collapse false + @selection.removeAllRanges() + @selection.addRange range + @selection.extend original.startContainer, original.startOffset when backward - @selection.collapseToStart() - @selection.modify "extend", forward, character for [0...length] + range = document.createRange() + range.setStart @selection.focusNode, @selection.focusOffset + range.setEnd @selection.anchorNode, @selection.anchorOffset + @selection.removeAllRanges() + @selection.addRange range # TODO(smblott). What do we do if there is no initial selection? Or multiple ranges? constructor: (options) -> -- cgit v1.2.3 From eefe8c29b2410119412984301eba8c66dffda059 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 15:32:10 +0000 Subject: Visual/edit modes: revert to slow "o". There are cases where the faster approach gets the wrong answer. --- content_scripts/mode_visual_edit.coffee | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 0f335a9b..657ae677 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -121,8 +121,21 @@ class Movement extends MaintainCount "o": -> # Swap the anchor and focus. - # Note(smblott). I can't find an approach which works for both cases, so we have to implement each case - # separately. + length = @selection.toString().length + switch @getDirection() + when forward + @selection.collapseToEnd() + # FIXME(smblott). This is super slow if the selection is large. + @selection.modify "extend", backward, character for [0...length] + when backward + @selection.collapseToStart() + @selection.modify "extend", forward, character for [0...length] + # Faster, but doesn't always work... + # @selection.extend @selection.anchorNode, length + return + # Note(smblott). I can't find an efficient approach which works for all cases, so we have to implement + # each case separately. + # FIXME: This is broken if the selection is in an input area. original = @selection.getRangeAt 0 switch @getDirection() when forward @@ -137,6 +150,7 @@ class Movement extends MaintainCount range.setEnd @selection.anchorNode, @selection.anchorOffset @selection.removeAllRanges() @selection.addRange range + return # TODO(smblott). What do we do if there is no initial selection? Or multiple ranges? constructor: (options) -> @@ -186,7 +200,7 @@ class VisualMode extends Movement return # Try to start with a visible selection. - if type == "Caret" or @selection.isCollapsed + if type == "Caret" # or @selection.isCollapsed (broken if selection is in and input) @moveInDirection(forward) or @moveInDirection backward defaults = -- cgit v1.2.3 From 256beee031efef70f4ee750044d9e697d66868bd Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 22 Jan 2015 16:50:58 +0000 Subject: Visual/edit modes: develop edit mode. - implement "i", "a". - fix "w" for edit mode. - try out "e" for enter edit mode. - initial implementation "o", "O" - Suppress backspace and delete. - Scroll in text areas. --- content_scripts/mode_visual_edit.coffee | 136 +++++++++++++++++--------------- 1 file changed, 71 insertions(+), 65 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 657ae677..e6ea968a 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -7,8 +7,12 @@ class SuppressPrintable extends Mode handler = (event) => if KeyboardUtils.isPrintable event if event.type == "keydown" - DomUtils.suppressPropagation - @stopBubblingAndFalse + # Completely suppress Backspace and Delete. + if event.keyCode in [ 8, 46 ] + @suppressEvent + else + DomUtils.suppressPropagation + @stopBubblingAndFalse else false else @@ -59,6 +63,13 @@ class Movement extends MaintainCount 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) + # Try to move one character in "direction". Return 1, -1 or 0, indicating that the selection got bigger, or # smaller, or is unchanged. moveInDirection: (direction) -> @@ -84,19 +95,19 @@ class Movement extends MaintainCount text.charAt if @getDirection() == forward then text.length - 1 else 0 moveByWord: (direction) -> - # We go to the end of the next word, then come back to the start of it. - movements = [ "#{direction} word", "#{@opposite[direction]} word" ] - # If we're in the middle of a word, then we also need to skip over that one. - movements.unshift "#{direction} word" unless /\s/.test @nextCharacter direction - @runMovements movements + @runMovement "#{direction} word" unless /\s/.test @nextCharacter direction + while /\s/.test @nextCharacter direction + break unless @selectionChanged => + @runMovement "#{direction} character" - # Run a movement command. Return true if the length of the selection changed, false otherwise. + # Run a movement command. runMovement: (movement) -> length = @selection.toString().length @selection.modify @alterMethod, movement.split(" ")... - @selection.toString().length != length + @alterMethod == "move" or @selection.toString().length != length runMovements: (movements) -> + console.log movements for movement in movements break unless @runMovement movement @@ -107,6 +118,7 @@ class Movement extends MaintainCount "k": "backward line" "e": "forward word" "b": "backward word" + "B": "backward word" ")": "forward sentence" "(": "backward sentence" "}": "forward paragraph" @@ -115,44 +127,16 @@ class Movement extends MaintainCount "0": "backward lineboundary" "G": "forward documentboundary" "g": "backward documentboundary" - "w": -> @moveByWord forward - "W": -> @moveByWord backward + "W": -> @moveByWord forward "o": -> - # Swap the anchor and focus. + # Swap the anchor and focus. This is too slow if the selection is large. + direction = @getDirection() length = @selection.toString().length - switch @getDirection() - when forward - @selection.collapseToEnd() - # FIXME(smblott). This is super slow if the selection is large. - @selection.modify "extend", backward, character for [0...length] - when backward - @selection.collapseToStart() - @selection.modify "extend", forward, character for [0...length] - # Faster, but doesn't always work... - # @selection.extend @selection.anchorNode, length - return - # Note(smblott). I can't find an efficient approach which works for all cases, so we have to implement - # each case separately. - # FIXME: This is broken if the selection is in an input area. - original = @selection.getRangeAt 0 - switch @getDirection() - when forward - range = original.cloneRange() - range.collapse false - @selection.removeAllRanges() - @selection.addRange range - @selection.extend original.startContainer, original.startOffset - when backward - range = document.createRange() - range.setStart @selection.focusNode, @selection.focusOffset - range.setEnd @selection.anchorNode, @selection.anchorOffset - @selection.removeAllRanges() - @selection.addRange range - return + @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]() + @selection.modify "extend", @opposite[direction], character for [0...length] - # TODO(smblott). What do we do if there is no initial selection? Or multiple ranges? constructor: (options) -> @alterMethod = options.alterMethod || "extend" super options @@ -171,10 +155,7 @@ class Movement extends MaintainCount @runMovement @movements[keyChar] when "function" @movements[keyChar].call @ - # Try to scroll the leading end of the selection into view. getLeadingElement() seems to work - # most, but not all, of the time. - leadingElement = @getLeadingElement @selection - Scroller.scrollIntoView leadingElement if leadingElement + @scrollIntoView() # Adapted from: http://roysharon.com/blog/37. # I have no idea how this works (smblott, 2015/1/22). @@ -190,6 +171,21 @@ class Movement extends MaintainCount t = o || t?.parentNode t + # Try to scroll the leading end of the selection into view. + scrollIntoView: -> + if document.activeElement and DomUtils.isEditable document.activeElement + 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 + # getLeadingElement() seems to work most, but not all, of the time. + leadingElement = @getLeadingElement @selection + Scroller.scrollIntoView leadingElement if leadingElement + class VisualMode extends Movement constructor: (options = {}) -> @selection = window.getSelection() @@ -207,7 +203,6 @@ class VisualMode extends Movement name: "visual" badge: "V" exitOnEscape: true - exitOnBlur: options.targetElement alterMethod: "extend" keypress: (event) => @@ -220,16 +215,18 @@ class VisualMode extends Movement handler: "copyToClipboard" data: text @exit() - handlerStack.push keyup: => false 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 super extend defaults, options @debug = true - # FIXME(smblott). + # 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 @@ -249,33 +246,42 @@ class EditMode extends Movement constructor: (options = {}) -> defaults = name: "edit" + badge: "E" exitOnEscape: true alterMethod: "move" - keydown: (event) => if @isActive() then @handleKeydown event else @continueBubbling - keypress: (event) => if @isActive() then @handleKeypress event else @continueBubbling - keyup: (event) => if @isActive() then @handleKeyup event else @continueBubbling + @debug = true @element = document.activeElement - if @element and DomUtils.isEditable @element - super extend defaults, options - - handleKeydown: (event) -> - @stopBubblingAndTrue - handleKeypress: (event) -> - @suppressEvent - handleKeyup: (event) -> - @stopBubblingAndTrue + return unless @element and DomUtils.isEditable @element + super extend defaults, options + handlerStack.debug = true - isActive: -> - document.activeElement and DomUtils.isDOMDescendant @element, document.activeElement + extend @movements, + "i": => @enterInsertMode() + "a": => @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 - updateBadge: (badge) -> - badge.badge = "E" if @isActive() + enterInsertMode: -> + new InsertMode + badge: "I" + blurOnEscape: false + + openLine: (direction) -> + @runMovement "#{direction} lineboundary" + @enterInsertMode() + @simulateTextEntry "\n" + @runMovement "backward character" if direction == backward + + simulateTextEntry: (text) -> + event = document.createEvent "TextEvent" + event.initTextEvent "textInput", true, true, null, text + document.activeElement.dispatchEvent event root = exports ? window root.VisualMode = VisualMode -- cgit v1.2.3 From e1b7b0a963490b0991d72a0143f489e0bc1e8096 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 23 Jan 2015 11:02:31 +0000 Subject: Visual/edit modes: more (and better) commands. --- content_scripts/mode_visual_edit.coffee | 267 +++++++++++++++++--------------- 1 file changed, 143 insertions(+), 124 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') 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 -- cgit v1.2.3 From 0f96b6bf07c02704272a90a017ee14661dca8ce0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 23 Jan 2015 17:19:19 +0000 Subject: Visual/edit modes: miscellaneous improvements. --- content_scripts/mode_visual_edit.coffee | 106 +++++++++++++++++++------------- 1 file changed, 64 insertions(+), 42 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index e2e9c5fe..d32ae24c 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -20,7 +20,7 @@ class SuppressPrintable extends Mode # This is pushed onto the handler stack before calling super(). Therefore, it ends up underneath (or # after) all of the other handlers associated with the mode. @suppressPrintableHandlerId = handlerStack.push - _name: "movement/suppress-printable" + _name: "#{@id}/suppress-printable" keydown: handler keypress: handler keyup: handler @@ -31,17 +31,20 @@ class SuppressPrintable extends Mode # This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. class MaintainCount extends SuppressPrintable constructor: (options) -> - @countPrefix = "" + @countPrefix = options.initialCount || "" super options @push - _name: "movement/maintain-count" + _name: "#{@id}/maintain-count" keypress: (event) => @alwaysContinueBubbling => unless event.metaKey or event.ctrlKey or event.altKey keyChar = String.fromCharCode event.charCode @countPrefix = if keyChar and keyChar.length == 1 and "0" <= keyChar <= "9" + if @options.initialCount + @countPrefix = "" + delete @options.initialCount @countPrefix + keyChar else "" @@ -112,17 +115,16 @@ class Movement extends MaintainCount "k": "backward line" "e": "forward word" "b": "backward word" - "B": "backward word" + "w": -> @moveForwardWord() ")": "forward sentence" "(": "backward sentence" "}": "forward paragraph" "{": "backward paragraph" "$": "forward lineboundary" "0": "backward lineboundary" - "w": -> @moveForwardWord() - "o": -> @swapFocusAndAnchor() "G": "forward documentboundary" "gg": "backward documentboundary" + "o": -> @swapFocusAndAnchor() constructor: (options) -> @movements = extend {}, @movements @@ -130,25 +132,37 @@ class Movement extends MaintainCount @alterMethod = options.alterMethod || "extend" @keyQueue = "" @yankedText = "" - super extend options, + super extend options + + @push + _name: "#{@id}/keypress" keypress: (event) => - @alwaysContinueBubbling => - unless event.metaKey or event.ctrlKey or event.altKey - @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 + unless event.metaKey or event.ctrlKey or event.altKey + @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 = "" + @selection = window.getSelection() + + 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 + @yank() + return @suppressEvent + + @continueBubbling # Aliases. @movements.B = @movements.b @@ -156,6 +170,7 @@ class Movement extends MaintainCount 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 @@ -166,6 +181,9 @@ class Movement extends MaintainCount @swapFocusAndAnchor() @lastYankedLine = @yank() + enterInsertMode: -> + new InsertMode { badge: "I", blurOnEscape: false } + # 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 @@ -224,28 +242,31 @@ class VisualMode extends Movement if @options.underEditMode extend @commands, "d": => @yank deleteFromDocument: true + "c": => @yank deleteFromDocument: true; @enterInsertMode() yank: (args...) -> text = super args... - unless @options.underEditMode - 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 + 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 + unless @options.underEditMode 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 + # Now set the clipboard. No operations which maniplulate the selection should follow this. chrome.runtime.sendMessage { handler: "copyToClipboard", data: @yankedText } if @yankedText +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 = {}) -> @@ -266,11 +287,15 @@ class EditMode extends Movement "O": => @openLine backward "p": => @pasteClipboard forward "P": => @pasteClipboard backward - "v": -> new VisualMode underEditMode: true - "yy": => @withRangeSelection => @yankLine() + "v": -> new VisualModeForEdit + "Y": => @withRangeSelection => @yankLine() + "y": => + new VisualModeForEdit + singleMovement: true + initialCount: @countPrefix - # Aliases. - @commands.Y = @commands.yy + # # Aliases. + # @commands.Y = @commands.yy pasteClipboard: (direction) -> text = Clipboard.paste @element @@ -287,9 +312,6 @@ class EditMode extends Movement DomUtils.simulateTextEntry @element, "\n" @runMovement "backward character" if direction == backward - enterInsertMode: -> - new InsertMode { badge: "I", blurOnEscape: false } - withRangeSelection: (func) -> @alterMethod = "extend" func.call @ -- cgit v1.2.3 From d9462e5d1e3ebb8f2fedc400b05c4c545fada142 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 24 Jan 2015 07:16:37 +0000 Subject: Visual/edit modes: more edit-mode commands. - "dw", "3dw", "d3w" - "dc", "3dc", "d3c" - "D" - "C" Also refactor enterInsertMode. Also major refactor of interface between edit and visual modes. --- content_scripts/mode_visual_edit.coffee | 197 ++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 74 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') 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 -- cgit v1.2.3 From 2b3c7e24bf81e0c945a9d394ef509a3160df0a79 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 24 Jan 2015 14:56:59 +0000 Subject: Visual/edit modes: minor changes. --- content_scripts/mode_visual_edit.coffee | 46 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index d870a69d..691a6681 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,4 +1,12 @@ +# To do: +# - handle edit-mode losing the focus +# - line-visual mode +# - definition of a line. +# - better implementation of `o` +# - caret mode +# - yy + # 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. class SuppressPrintable extends Mode @@ -19,14 +27,14 @@ class SuppressPrintable extends Mode # This is pushed onto the handler stack before calling super(). Therefore, it ends up underneath (or # after) all of the other handlers associated with the mode. - @suppressPrintableHandlerId = handlerStack.push + handlerId = handlerStack.push _name: "#{@id}/suppress-printable" keydown: handler keypress: handler keyup: handler super options - @onExit => handlerStack.remove @suppressPrintableHandlerId + @handlers.push handlerId # This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. class MaintainCount extends SuppressPrintable @@ -41,7 +49,7 @@ class MaintainCount extends SuppressPrintable unless event.metaKey or event.ctrlKey or event.altKey keyChar = String.fromCharCode event.charCode @countPrefix = - if keyChar and keyChar.length == 1 and "0" <= keyChar <= "9" + if keyChar?.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0" if @options.initialCount @countPrefix = "" delete @options.initialCount @@ -49,10 +57,6 @@ class MaintainCount extends SuppressPrintable else "" - runCountPrefixTimes: (func) -> - count = if 0 < @countPrefix.length then parseInt @countPrefix else 1 - func() for [0...count] - # Some symbolic names. forward = "forward" backward = "backward" @@ -75,9 +79,12 @@ class Movement extends MaintainCount # 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) - not (r.compareBoundaryPoints(Range.END_TO_END, rr) or r.compareBoundaryPoints Range.START_TO_START, rr) + rr = @selection.getRangeAt 0 + rr.startContainer != r.startContainer or + rr. startOffset != r.startOffset or + @selection.toString().length != length # Try to move one character in "direction". Return 1, -1 or 0, indicating whether the selection got bigger, # or smaller, or is unchanged. @@ -146,7 +153,7 @@ class Movement extends MaintainCount "$": "forward lineboundary" "0": "backward lineboundary" "G": "forward documentboundary" - "gg": "backward documentboundary" + "g": "backward documentboundary" "Y": -> @selectLine() "o": -> @swapFocusAndAnchor() @@ -195,18 +202,21 @@ class Movement extends MaintainCount @yank() return @suppressEvent - @scrollIntoView() break @continueBubbling handleMovementKeyChar: (keyChar) -> + # We need to copy the count prefix immediately, because protectClipboard is asynchronous. + count = if 0 < @countPrefix.length then parseInt @countPrefix else 1 + @countPrefix = "" if @movements[keyChar] @protectClipboard => - @runCountPrefixTimes => + for [0...count] switch typeof @movements[keyChar] when "string" then @runMovement @movements[keyChar] when "function" then @movements[keyChar].call @ + @scrollIntoView() yank: (args = {}) -> @yankedText = window.getSelection().toString() @@ -237,10 +247,11 @@ class Movement extends MaintainCount for direction in [ forward, backward ] @runMovement "#{direction} lineboundary" @swapFocusAndAnchor() + Movement.lastLineSelection = @selection.toString() + console.log "lastLineSelection:", Movement.lastLineSelection yankLine: -> @selectLine() - @lastYankedLine = @yank() # Adapted from: http://roysharon.com/blog/37. # I have no idea how this works (smblott, 2015/1/22). @@ -329,7 +340,7 @@ class EditMode extends Movement "v": -> new VisualModeForEdit "Y": -> @runInVisualMode runMovement: "Y" - "y": => @runInVisualMode() + "y": => @runInVisualMode {} "d": => @runInVisualMode deleteFromDocument: true "c": => @runInVisualMode deleteFromDocument: true @@ -352,11 +363,12 @@ class EditMode extends Movement @protectClipboard => @paste (text) => if text - if text == @lastYankedLine - text += "\n" + lineOriented = text == Movement.lastLineSelection + if lineOriented @runMovement "#{direction} lineboundary" - @runMovement "#{direction} character" if direction == forward + text = if direction == forward then "\n" + text else text + "\n" DomUtils.simulateTextEntry @element, text + @runMovement "backward line" if lineOriented openLine: (direction) -> @runMovement "#{direction} lineboundary" -- cgit v1.2.3 From f7bf189fc0e918a4d46e0eb375dcfe95dc380676 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 24 Jan 2015 16:39:13 +0000 Subject: Visual/edit modes: for "$", advance to real end of line. --- content_scripts/mode_visual_edit.coffee | 62 ++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 691a6681..f45ecf51 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,8 +1,7 @@ # To do: -# - handle edit-mode losing the focus +# - edit-mode losing the focus # - line-visual mode -# - definition of a line. # - better implementation of `o` # - caret mode # - yy @@ -122,7 +121,7 @@ class Movement extends MaintainCount if @alterMethod == "move" and not locked locked = true @paste (text) => - result = func() + result = func text @copy text locked = false result @@ -131,7 +130,25 @@ class Movement extends MaintainCount # Run a movement command. Return true if the selection changed, false otherwise. runMovement: (movement) -> - @selectionChanged => @selection.modify @alterMethod, movement.split(" ")... + @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) -> @@ -188,7 +205,6 @@ 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() @@ -244,14 +260,10 @@ class Movement extends MaintainCount @copy @yankedText if @yankedText selectLine: -> - for direction in [ forward, backward ] + direction = @getDirection() + for direction in [ @opposite[direction], direction ] @runMovement "#{direction} lineboundary" @swapFocusAndAnchor() - Movement.lastLineSelection = @selection.toString() - console.log "lastLineSelection:", Movement.lastLineSelection - - yankLine: -> - @selectLine() # Adapted from: http://roysharon.com/blog/37. # I have no idea how this works (smblott, 2015/1/22). @@ -305,14 +317,12 @@ class VisualMode extends Movement underEditMode: false super extend defaults, options - extend @commands, - "y": @yank - "Y": @yankLine + extend @commands, "y": @yank if @options.underEditMode extend @commands, - "d": => @yank deleteFromDocument: true - "c": => @yank deleteFromDocument: true; enterInsertMode() + "d": @yank + "c": -> @yank(); enterInsertMode() class VisualModeForEdit extends VisualMode constructor: (options = {}) -> @@ -360,15 +370,17 @@ class EditMode extends Movement @countPrefix = "" pasteClipboard: (direction) -> - @protectClipboard => - @paste (text) => - if text - lineOriented = text == Movement.lastLineSelection - if lineOriented - @runMovement "#{direction} lineboundary" - text = if direction == forward then "\n" + text else text + "\n" - DomUtils.simulateTextEntry @element, text - @runMovement "backward line" if lineOriented + @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 openLine: (direction) -> @runMovement "#{direction} lineboundary" -- cgit v1.2.3 From 4d47050983c46fc6563b40517aef949b7ad46b54 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 24 Jan 2015 17:26:35 +0000 Subject: Visual/edit modes: implement "yy". --- content_scripts/mode_visual_edit.coffee | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index f45ecf51..ad7fb59c 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -4,7 +4,6 @@ # - line-visual mode # - better implementation of `o` # - caret mode -# - yy # 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. @@ -262,6 +261,7 @@ class Movement extends MaintainCount selectLine: -> direction = @getDirection() for direction in [ @opposite[direction], direction ] + console.log direction @runMovement "#{direction} lineboundary" @swapFocusAndAnchor() @@ -317,7 +317,11 @@ class VisualMode extends Movement underEditMode: false super extend defaults, options - extend @commands, "y": @yank + extend @commands, + "y": -> + # Special case: "yy" (the first from edit mode, and now the second). + @selectLine() if @options.expectImmediateY and @keyQueue == "" + @yank() if @options.underEditMode extend @commands, @@ -350,7 +354,7 @@ class EditMode extends Movement "v": -> new VisualModeForEdit "Y": -> @runInVisualMode runMovement: "Y" - "y": => @runInVisualMode {} + "y": => @runInVisualMode expectImmediateY: true "d": => @runInVisualMode deleteFromDocument: true "c": => @runInVisualMode deleteFromDocument: true -- cgit v1.2.3 From 9c4603680284a4e00f11b097aebc65c6f6c0af07 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 24 Jan 2015 17:50:47 +0000 Subject: Visual/edit modes: visual line mode. --- content_scripts/mode_visual_edit.coffee | 256 +++++++++++++++----------------- 1 file changed, 118 insertions(+), 138 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') 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 -- cgit v1.2.3 From 05351d4650ace35574bf4f9c55cced65950252f5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 09:37:29 +0000 Subject: Visual/edit modes: find (experimental). The UX around find in visual mode can be a bit weird. It may be better to remove it. --- content_scripts/mode_visual_edit.coffee | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 12d8bf4a..261166f9 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -2,7 +2,7 @@ # To do: # - better implementation of `o` # - caret mode -# - find operations +# - find operations (needs better implementation?) # 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. @@ -286,6 +286,29 @@ class VisualMode extends Movement "d": -> @yank deleteFromDocument: true "c": -> @yank(); enterInsertMode() + # Map "n" and "N" for poor-man's find. + unless @options.underEditMode + do => + findBackwards = false + query = getFindModeQuery() + return unless query + + executeFind = => @protectClipboard => + initialRange = @selection.getRangeAt(0).cloneRange() + caseSensitive = /[A-Z]/.test query + if query + window.find query, caseSensitive, findBackwards, true, false, true, false + newRange = @selection.getRangeAt(0).cloneRange() + range = document.createRange() + range.setStart initialRange.startContainer, initialRange.startOffset + range.setEnd newRange.endContainer, newRange.endOffset + @selection.removeAllRanges() + @selection.addRange range + + extend @movements, + "n": -> executeFind() + "N": -> findBackwards = not findBackwards; executeFind() + @clipboardContents = "" @paste (text) => @clipboardContents = text -- cgit v1.2.3 From 9b3470422695adbe3067c0b57dacdfeb324d5587 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 11:01:04 +0000 Subject: Visual/edit modes: better reverseSelection(). There's an efficient way to implement reverseSelection, and an inefficient way. Unfortunately, the efficient way does not work for text inputs like textareas. So we fall back to the inefficient method in that case. --- content_scripts/mode_visual_edit.coffee | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 261166f9..6f327807 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -73,6 +73,27 @@ class Movement extends MaintainCount chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response + # Swap the anchor node/offset and the focus node/offset. + reverseSelection: -> + element = document.activeElement + if element and DomUtils.isEditable(element) and not element. isContentEditable + # Note(smblott). This implementation is unacceptably inefficient if the selection is large. We only use + # it if we have to. However, the normal method does not work for input elements. + direction = @getDirection() + length = @selection.toString().length + @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]() + @selection.modify "extend", @opposite[direction], character for [0...length] + else + # Normal method. + direction = @getDirection() + original = @selection.getRangeAt(0).cloneRange() + range = original.cloneRange() + range.collapse direction == backward + @selection.removeAllRanges() + @selection.addRange range + which = if direction == forward then "start" else "end" + @selection.extend original["#{which}Container"], original["#{which}Offset"], + # Run a movement command. runMovement: (movement) -> @selection.modify @alterMethod, movement.split(" ")... @@ -105,14 +126,6 @@ class Movement extends MaintainCount 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] - movements: "l": "forward character" "h": "backward character" @@ -322,7 +335,7 @@ class VisualMode extends Movement class VisualLineMode extends VisualMode constructor: (options = {}) -> super options - @selectLine() + @selectLine() unless @selection?.type == "None" handleMovementKeyChar: (keyChar) -> super keyChar -- cgit v1.2.3 From 7b6354550f1d92825f22afccabdea9a6c39d4b0b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 11:51:34 +0000 Subject: Visual/edit modes: experimental "x" implementation. --- content_scripts/mode_visual_edit.coffee | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 6f327807..2dd274a9 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -372,6 +372,11 @@ class EditMode extends Movement "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode + "x": => + if 0 < @selection.toString().length + @copy @selection.toString() + @selection.deleteFromDocument() + enterVisualMode: (options = {}) -> defaults = underEditMode: true -- cgit v1.2.3 From 57f0a4bbfeba97e3ecfaa6027c51e51d70756ad9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 12:55:26 +0000 Subject: Visual/edit modes: implement "V" in visual mode. --- content_scripts/mode_visual_edit.coffee | 2 ++ 1 file changed, 2 insertions(+) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 2dd274a9..5d617437 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -284,6 +284,7 @@ class VisualMode extends Movement defaults = name: "visual" badge: "V" + singleton: VisualMode exitOnEscape: true alterMethod: "extend" super extend defaults, options @@ -293,6 +294,7 @@ class VisualMode extends Movement # Special case: "yy" (the first from edit mode, and now the second). @selectLine() if @options.expectImmediateY and @keyQueue == "" @yank() + "V": -> new VisualLineMode @options if @options.underEditMode extend @commands, -- cgit v1.2.3 From f993daf357ea85183bcf17cbe7269284cacddd8e Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 14:24:36 +0000 Subject: Visual/edit modes: better mode changes on blur/focus. --- content_scripts/mode_visual_edit.coffee | 54 ++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 14 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 5d617437..05514a15 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -4,6 +4,9 @@ # - caret mode # - find operations (needs better implementation?) +enterInsertMode = -> + new InsertMode { badge: "I", blurOnEscape: false } + # 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. class SuppressPrintable extends Mode @@ -211,7 +214,7 @@ class Movement extends MaintainCount yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if args.deleteFromDocument or @options.deleteFromDocument - console.log "yank:", @yankedText + console.log "yank:", @yankedText if @debug message = @yankedText.replace /\s+/g, " " length = message.length @@ -219,15 +222,15 @@ class Movement extends MaintainCount plural = if length == 1 then "" else "s" HUD.showForDuration "Yanked #{length} character#{plural}: \"#{message}\".", 2500 - @options.onYank.call @ @yankedText if @options.onYank + @options.onYank.call @, @yankedText if @options.onYank @exit() @yankedText exit: (event, target) -> super event, target - unless @options.underEditMode - if document.activeElement and DomUtils.isEditable document.activeElement - document.activeElement.blur() + # unless @options.underEditMode + # if document.activeElement and DomUtils.isEditable document.activeElement + # document.activeElement.blur() unless event?.type == "keydown" and KeyboardUtils.isEscape event if 0 < @selection.toString().length @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() @@ -348,11 +351,13 @@ class EditMode extends Movement @element = document.activeElement return unless @element and DomUtils.isEditable @element - super + defaults = name: "edit" badge: "E" exitOnEscape: true + exitOnBlur: @element alterMethod: "move" + super extend defaults, options extend @commands, "i": enterInsertMode @@ -397,12 +402,6 @@ class EditMode extends Movement DomUtils.simulateTextEntry @element, "\n" @runMovement "backward character" if direction == backward - exit: (event, target) -> - super() - 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 -> @@ -420,8 +419,35 @@ class EditMode extends Movement @copy clipboard locked = false -enterInsertMode = -> - new InsertMode { badge: "I", blurOnEscape: false } + exit: (event, target) -> + super() + if event?.type == "keydown" and KeyboardUtils.isEscape event + if target? and DomUtils.isDOMDescendant @element, target + @element.blur() + if event?.type == "blur" + new BlurredEditMode @options + +# In edit mode, the input blurs if the user changes tabs or clicks outside of the element. In the former +# case, the user expects to remain in edit mode. In the latter case, they may just be copying some text with +# the mouse/Ctrl-C, and again they expect to remain in edit mode when they return. BlurredEditMode monitors +# various events and tries to either exit completely or re-enter edit mode as appropriate. +class BlurredEditMode extends Mode + constructor: (originalOptions) -> + super + name: "blurred-edit" + singleton: originalOptions.singleton + + @push + _name: "#{@id}/focus" + focus: (event) => + @alwaysContinueBubbling => + if event?.target == originalOptions.singleton + console.log "#{@id}: reactivating edit mode" if @debug + new EditMode originalOptions + keypress: (event) => + @alwaysContinueBubbling => + @exit() unless event.metaKey or event.ctrlKey or event.altKey + root = exports ? window root.VisualMode = VisualMode -- cgit v1.2.3 From 6c2580ae4c9ab14d0b6340c45dd3f5cd05fce2d7 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 15:06:26 +0000 Subject: Visual/edit modes: miscellaneous changes. --- content_scripts/mode_visual_edit.coffee | 74 +++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 31 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 05514a15..7d62b784 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,8 +1,6 @@ # To do: -# - better implementation of `o` # - caret mode -# - find operations (needs better implementation?) enterInsertMode = -> new InsertMode { badge: "I", blurOnEscape: false } @@ -95,7 +93,7 @@ class Movement extends MaintainCount @selection.removeAllRanges() @selection.addRange range which = if direction == forward then "start" else "end" - @selection.extend original["#{which}Container"], original["#{which}Offset"], + @selection.extend original["#{which}Container"], original["#{which}Offset"] # Run a movement command. runMovement: (movement) -> @@ -117,7 +115,7 @@ class Movement extends MaintainCount if success = @moveInDirection direction @moveInDirection @opposite[direction] return if 0 < success then direction else @opposite[direction] - forward + backward # An approximation of the vim "w" movement. moveForwardWord: (direction) -> @@ -265,7 +263,7 @@ class Movement extends MaintainCount 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 @@ -295,7 +293,7 @@ class VisualMode extends Movement extend @commands, "y": -> # Special case: "yy" (the first from edit mode, and now the second). - @selectLine() if @options.expectImmediateY and @keyQueue == "" + @selectLine() if @options.yYanksLine @yank() "V": -> new VisualLineMode @options @@ -303,29 +301,8 @@ class VisualMode extends Movement extend @commands, "d": -> @yank deleteFromDocument: true "c": -> @yank(); enterInsertMode() - - # Map "n" and "N" for poor-man's find. - unless @options.underEditMode - do => - findBackwards = false - query = getFindModeQuery() - return unless query - - executeFind = => @protectClipboard => - initialRange = @selection.getRangeAt(0).cloneRange() - caseSensitive = /[A-Z]/.test query - if query - window.find query, caseSensitive, findBackwards, true, false, true, false - newRange = @selection.getRangeAt(0).cloneRange() - range = document.createRange() - range.setStart initialRange.startContainer, initialRange.startOffset - range.setEnd newRange.endContainer, newRange.endOffset - @selection.removeAllRanges() - @selection.addRange range - - extend @movements, - "n": -> executeFind() - "N": -> findBackwards = not findBackwards; executeFind() + else + @installFindMode() @clipboardContents = "" @paste (text) => @clipboardContents = text @@ -337,8 +314,43 @@ class VisualMode extends Movement copy: (text) -> super @clipboardContents = text + installFindMode: -> + previousFindRange = null + + executeFind = (findBackwards) => + query = getFindModeQuery() + if query + caseSensitive = Utils.hasUpperCase query + @protectClipboard => + initialRange = @selection.getRangeAt(0).cloneRange() + direction = @getDirection() + which = if direction == forward then "start" else "end" + + if previousFindRange + @selection.removeAllRanges() + @selection.addRange previousFindRange + + window.find query, caseSensitive, findBackwards, true, false, true, false + previousFindRange = newFindRange = @selection.getRangeAt(0).cloneRange() + + range = document.createRange() + range.setStart initialRange["#{which}Container"], initialRange["#{which}Offset"] + range.setEnd newFindRange.endContainer, newFindRange.endOffset + @selection.removeAllRanges() + @selection.addRange range + + if @getDirection() == backward or @selection.toString().length == 0 + range.setStart newFindRange.startContainer, newFindRange.startOffset + @selection.removeAllRanges() + @selection.addRange range + + extend @movements, + "n": -> executeFind false + "N": -> executeFind true + class VisualLineMode extends VisualMode constructor: (options = {}) -> + options.name = "visual/line" super options @selectLine() unless @selection?.type == "None" @@ -370,11 +382,11 @@ class EditMode extends Movement "v": -> new VisualMode underEditMode: true "Y": -> @enterVisualMode runMovement: "Y" - "y": => @enterVisualMode expectImmediateY: true + "y": => @enterVisualMode yYanksLine: true "d": => @enterVisualMode deleteFromDocument: true "c": => @enterVisualMode deleteFromDocument: true - onYank: -> new InsertMode { badge: "I", blurOnEscape: false } + onYank: enterInsertMode "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode -- cgit v1.2.3 From 9ab52af747c55608aae27069e416c45111da9be1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 16:20:37 +0000 Subject: Visual/edit modes: establish an initial selection. --- content_scripts/mode_visual_edit.coffee | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 7d62b784..d909bf43 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -276,8 +276,9 @@ class VisualMode extends Movement @selection = window.getSelection() switch @selection.type when "None" - HUD.showForDuration "Create a selection before entering visual mode.", 2500 - return + unless @establishInitialSelection() + HUD.showForDuration "Create a selection before entering visual mode.", 2500 + return when "Caret" # Try to start with a visible selection. @moveInDirection(forward) or @moveInDirection backward unless options.underEditMode @@ -295,7 +296,7 @@ class VisualMode extends Movement # Special case: "yy" (the first from edit mode, and now the second). @selectLine() if @options.yYanksLine @yank() - "V": -> new VisualLineMode @options + "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange() if @options.underEditMode extend @commands, @@ -348,11 +349,33 @@ class VisualMode extends Movement "n": -> executeFind false "N": -> executeFind true + establishInitialSelection: -> + nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT + while node = nodes.nextNode() + if node.nodeType == 3 and 50 <= node.data.trim().length + element = node.parentElement + if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element + range = document.createRange() + text = node.data + trimmed = text.replace /^\s+/, "" + offset = text.length - trimmed.length + range.setStart node, offset + range.setEnd node, offset + 1 + @selection.removeAllRanges() + @selection.addRange range + @scrollIntoView() + return true + false + class VisualLineMode extends VisualMode constructor: (options = {}) -> options.name = "visual/line" super options - @selectLine() unless @selection?.type == "None" + unless @selection?.type == "None" + if options.initialRange + @selection.removeAllRanges() + @selection.addRange options.initialRange + @selectLine() handleMovementKeyChar: (keyChar) -> super keyChar -- cgit v1.2.3 From fa8cc2250e8b8956e62e1474ee0aa08e433accba Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 16:41:01 +0000 Subject: Visual/edit modes: enter insert mode directly. --- content_scripts/mode_visual_edit.coffee | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index d909bf43..91da063b 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,7 +1,4 @@ -# To do: -# - caret mode - enterInsertMode = -> new InsertMode { badge: "I", blurOnEscape: false } @@ -95,7 +92,8 @@ class Movement extends MaintainCount which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Run a movement command. + # Run a movement command. The movement should be a string of the form "direction amount", e.g. "forward + # word". runMovement: (movement) -> @selection.modify @alterMethod, movement.split(" ")... @@ -119,13 +117,7 @@ class Movement extends MaintainCount # An approximation of the vim "w" movement. moveForwardWord: (direction) -> - # 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 + @runMovement movement for movement in [ "forward word", "forward word", "backward word" ] movements: "l": "forward character" @@ -352,6 +344,7 @@ class VisualMode extends Movement establishInitialSelection: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT while node = nodes.nextNode() + # Try not to pick really small nodes. They're likely to be part of a banner. if node.nodeType == 3 and 50 <= node.data.trim().length element = node.parentElement if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element @@ -419,6 +412,12 @@ class EditMode extends Movement @copy @selection.toString() @selection.deleteFromDocument() + # If the input is empty, then enter insert mode immediately + unless @element.isContentEditable + if @element.value.trim() == "" + enterInsertMode() + HUD.showForDuration "Input empty, entered insert mode directly.", 3500 + enterVisualMode: (options = {}) -> defaults = underEditMode: true -- cgit v1.2.3 From 7bf8a6a4a9bf10985f0f211fda72a1ee759ab389 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 25 Jan 2015 17:08:17 +0000 Subject: Visual/edit modes: miscellaneous improvements. --- content_scripts/mode_visual_edit.coffee | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 91da063b..7aa6858d 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -144,6 +144,7 @@ class Movement extends MaintainCount @commands = {} @alterMethod = options.alterMethod || "extend" @keyQueue = "" + @keyPressCount = 0 @yankedText = "" super options @@ -160,6 +161,7 @@ class Movement extends MaintainCount @push _name: "#{@id}/keypress" keypress: (event) => + @keyPressCount += 1 unless event.metaKey or event.ctrlKey or event.altKey @keyQueue += String.fromCharCode event.charCode # We allow at most three characters for a command or movement mapping. @@ -274,6 +276,7 @@ class VisualMode extends Movement when "Caret" # Try to start with a visible selection. @moveInDirection(forward) or @moveInDirection backward unless options.underEditMode + @scrollIntoView() if @selection.type == "Range" defaults = name: "visual" @@ -284,25 +287,33 @@ class VisualMode extends Movement super extend defaults, options extend @commands, + "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange() "y": -> # Special case: "yy" (the first from edit mode, and now the second). - @selectLine() if @options.yYanksLine + @selectLine() if @options.yYanksLine and @keyPressCount == 1 @yank() - "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange() if @options.underEditMode extend @commands, - "d": -> @yank deleteFromDocument: true "c": -> @yank(); enterInsertMode() - else + "d": -> + # Special case: "dd" (the first from edit mode, and now the second). + @selectLine() if @options.dYanksLine and @keyPressCount == 1 + @yank deleteFromDocument: true + + unless @options.underEditMode @installFindMode() + # Grab the initial clipboard contents. We'll try to keep them intact until we get an explicit yank. @clipboardContents = "" - @paste (text) => @clipboardContents = text + @paste (text) => + @clipboardContents = text if text + # + # End of VisualMode constructor. protectClipboard: (func) -> func() - @copy @clipboardContents + @copy @clipboardContents if @clipboardContents copy: (text) -> super @clipboardContents = text @@ -399,10 +410,8 @@ class EditMode extends Movement "Y": -> @enterVisualMode runMovement: "Y" "y": => @enterVisualMode yYanksLine: true - "d": => @enterVisualMode deleteFromDocument: true - "c": => @enterVisualMode - deleteFromDocument: true - onYank: enterInsertMode + "d": => @enterVisualMode deleteFromDocument: true, dYanksLine: true + "c": => @enterVisualMode deleteFromDocument: true, onYank: enterInsertMode "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode @@ -420,6 +429,7 @@ class EditMode extends Movement enterVisualMode: (options = {}) -> defaults = + badge: "" underEditMode: true initialCount: @countPrefix oneMovementOnly: true -- cgit v1.2.3 From 1a83f98dcbea073ddf0221fe57fcad1d1c198129 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 06:07:56 +0000 Subject: Visual/edit modes: "x" and entity moves ("daw", "cas", etc.) --- content_scripts/mode_visual_edit.coffee | 48 +++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 20 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 7aa6858d..8f7f3ded 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -92,10 +92,11 @@ class Movement extends MaintainCount which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Run a movement command. The movement should be a string of the form "direction amount", e.g. "forward - # word". + # Run a movement command. The single movement argument can be a string of the form "direction amount", e.g. + # "forward word", or a list, e.g. [ "forward", "word" ]. runMovement: (movement) -> - @selection.modify @alterMethod, movement.split(" ")... + movement = movement.split(" ") if typeof movement == "string" + @selection.modify @alterMethod, movement... # Try to move one character in "direction". Return 1, -1 or 0, indicating whether the selection got bigger, # or smaller, or is unchanged. @@ -135,7 +136,7 @@ class Movement extends MaintainCount "G": "forward documentboundary" "g": "backward documentboundary" "w": -> @moveForwardWord() - "Y": -> @selectLine() + "Y": -> @selectLexicalEntity "lineboundary" "o": -> @reverseSelection() constructor: (options) -> @@ -228,10 +229,12 @@ class Movement extends MaintainCount @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() @copy @yankedText if @yankedText - selectLine: -> + # Select a lexical entity, such as a word, a line, or a sentence. The argument should be a movement target, + # such as "word" or "lineboundary". + selectLexicalEntity: (entity) -> for direction in [ backward, forward ] @reverseSelection() - @runMovement "#{direction} lineboundary" + @runMovement [ direction, entity ] # Try to scroll the focus into view. scrollIntoView: -> @@ -290,17 +293,25 @@ class VisualMode extends Movement "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange() "y": -> # Special case: "yy" (the first from edit mode, and now the second). - @selectLine() if @options.yYanksLine and @keyPressCount == 1 + @selectLexicalEntity "lineboundary" if @options.yYanksLine and @keyPressCount == 1 @yank() - if @options.underEditMode + if @options.underEditMode and not @options.oneMovementOnly extend @commands, "c": -> @yank(); enterInsertMode() + "x": -> @yank deleteFromDocument: true "d": -> # Special case: "dd" (the first from edit mode, and now the second). - @selectLine() if @options.dYanksLine and @keyPressCount == 1 + @selectLexicalEntity "lineboundary" if @options.dYanksLine and @keyPressCount == 1 @yank deleteFromDocument: true + if @options.oneMovementOnly + extend @commands, + "a": -> + if @keyPressCount == 1 + for entity in [ "word", "sentence", "paragraph" ] + do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity + unless @options.underEditMode @installFindMode() @@ -379,7 +390,7 @@ class VisualLineMode extends VisualMode if options.initialRange @selection.removeAllRanges() @selection.addRange options.initialRange - @selectLine() + @selectLexicalEntity "lineboundary" handleMovementKeyChar: (keyChar) -> super keyChar @@ -409,6 +420,7 @@ class EditMode extends Movement "v": -> new VisualMode underEditMode: true "Y": -> @enterVisualMode runMovement: "Y" + "x": -> @enterVisualMode runMovement: "h", deleteFromDocument: true "y": => @enterVisualMode yYanksLine: true "d": => @enterVisualMode deleteFromDocument: true, dYanksLine: true "c": => @enterVisualMode deleteFromDocument: true, onYank: enterInsertMode @@ -416,16 +428,12 @@ class EditMode extends Movement "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode - "x": => - if 0 < @selection.toString().length - @copy @selection.toString() - @selection.deleteFromDocument() - - # If the input is empty, then enter insert mode immediately - unless @element.isContentEditable - if @element.value.trim() == "" - enterInsertMode() - HUD.showForDuration "Input empty, entered insert mode directly.", 3500 + # Disabled as potentially confusing. + # # If the input is empty, then enter insert mode immediately + # unless @element.isContentEditable + # if @element.value.trim() == "" + # enterInsertMode() + # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 enterVisualMode: (options = {}) -> defaults = -- cgit v1.2.3 From ef9a8473d7d6b932de21642684f27e7696aac01b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 06:31:34 +0000 Subject: Visual/edit modes: minor changes. --- content_scripts/mode_visual_edit.coffee | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 8f7f3ded..3659c7ba 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -239,15 +239,14 @@ class Movement extends MaintainCount # 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? + 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. else - position = if @getDirection() == backward then element.selectionStart else element.selectionEnd - coords = DomUtils.getCaretCoordinates element, position - Scroller.scrollToPosition element, coords.top, coords.left + 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 -- cgit v1.2.3 From f644d622ffd7b8ce332d4c2470cd52772abadb2a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 10:15:33 +0000 Subject: Visual/edit modes: better recovery after focus change. If we're in edit mode and the user changes tab, we lose the focus. That causes edit mode (and any sub mode like visual mode) to exit. When we return, we're in insert mode! With this commit, we save the state, and restore it when appropriate. --- content_scripts/mode_visual_edit.coffee | 139 +++++++++++++++++++------------- 1 file changed, 83 insertions(+), 56 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 3659c7ba..1271a7fe 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,7 +1,4 @@ -enterInsertMode = -> - new InsertMode { badge: "I", blurOnEscape: false } - # 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. class SuppressPrintable extends Mode @@ -120,6 +117,10 @@ class Movement extends MaintainCount moveForwardWord: (direction) -> @runMovement movement for movement in [ "forward word", "forward word", "backward word" ] + collapseSelection: -> + if 0 < @selection.toString().length + @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() + movements: "l": "forward character" "h": "backward character" @@ -219,16 +220,6 @@ class Movement extends MaintainCount @exit() @yankedText - exit: (event, target) -> - super event, target - # unless @options.underEditMode - # if document.activeElement and DomUtils.isEditable document.activeElement - # document.activeElement.blur() - 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 - # Select a lexical entity, such as a word, a line, or a sentence. The argument should be a movement target, # such as "word" or "lineboundary". selectLexicalEntity: (entity) -> @@ -270,15 +261,20 @@ class Movement extends MaintainCount class VisualMode extends Movement constructor: (options = {}) -> @selection = window.getSelection() - switch @selection.type - when "None" - unless @establishInitialSelection() - HUD.showForDuration "Create a selection before entering visual mode.", 2500 - return - when "Caret" - # Try to start with a visible selection. - @moveInDirection(forward) or @moveInDirection backward unless options.underEditMode - @scrollIntoView() if @selection.type == "Range" + + if options.initialRange + @selection.removeAllRanges() + @selection.addRange options.initialRange + else + switch @selection.type + when "None" + unless @establishInitialSelection() + HUD.showForDuration "Create a selection before entering visual mode.", 2500 + return + when "Caret" + # Try to start with a visible selection. + @moveInDirection(forward) or @moveInDirection backward unless options.editModeParent + @scrollIntoView() if @selection.type == "Range" defaults = name: "visual" @@ -289,15 +285,15 @@ class VisualMode extends Movement super extend defaults, options extend @commands, - "V": -> new VisualLineMode extend @options, initialRange: @selection.getRangeAt(0).cloneRange() + "V": -> new VisualLineMode initialRange: @selection.getRangeAt(0).cloneRange() "y": -> # Special case: "yy" (the first from edit mode, and now the second). @selectLexicalEntity "lineboundary" if @options.yYanksLine and @keyPressCount == 1 @yank() - if @options.underEditMode and not @options.oneMovementOnly + if @options.editModeParent and not @options.oneMovementOnly extend @commands, - "c": -> @yank(); enterInsertMode() + "c": -> @yank deleteFromDocument: true; @options.editModeParent.enterInsertMode() "x": -> @yank deleteFromDocument: true "d": -> # Special case: "dd" (the first from edit mode, and now the second). @@ -311,7 +307,7 @@ class VisualMode extends Movement for entity in [ "word", "sentence", "paragraph" ] do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity - unless @options.underEditMode + unless @options.editModeParent @installFindMode() # Grab the initial clipboard contents. We'll try to keep them intact until we get an explicit yank. @@ -328,6 +324,26 @@ class VisualMode extends Movement copy: (text) -> super @clipboardContents = text + exit: (event, target) -> + if @options.editModeParent + if event?.type == "keydown" and KeyboardUtils.isEscape event + # Return to a caret for edit mode. + @collapseSelection() + + @collapseSelection() if @yankedText + + unless @options.editModeParent + # Don't leave the user in insert mode just because they happen to have selected text within an input + # element. + if document.activeElement and DomUtils.isEditable document.activeElement + document.activeElement.blur() + + super event, target + # Copying the yanked text to the clipboard must be the very last thing we do, because other operations + # (like collapsing the selection) interfere with the clipboard. + @copy @yankedText if @yankedText + + installFindMode: -> previousFindRange = null @@ -386,9 +402,6 @@ class VisualLineMode extends VisualMode options.name = "visual/line" super options unless @selection?.type == "None" - if options.initialRange - @selection.removeAllRanges() - @selection.addRange options.initialRange @selectLexicalEntity "lineboundary" handleMovementKeyChar: (keyChar) -> @@ -409,39 +422,46 @@ class EditMode extends Movement super extend defaults, options extend @commands, - "i": enterInsertMode - "a": enterInsertMode - "A": => @runMovement "forward lineboundary"; enterInsertMode() - "o": => @openLine forward - "O": => @openLine backward - "p": => @pasteClipboard forward - "P": => @pasteClipboard backward - "v": -> new VisualMode underEditMode: true + "i": -> @enterInsertMode() + "a": -> @enterInsertMode() + "A": -> @runMovement "forward lineboundary"; @enterInsertMode() + "o": -> @openLine forward + "O": -> @openLine backward + "p": -> @pasteClipboard forward + "P": -> @pasteClipboard backward + "v": -> @launchSubMode VisualMode "Y": -> @enterVisualMode runMovement: "Y" "x": -> @enterVisualMode runMovement: "h", deleteFromDocument: true - "y": => @enterVisualMode yYanksLine: true - "d": => @enterVisualMode deleteFromDocument: true, dYanksLine: true - "c": => @enterVisualMode deleteFromDocument: true, onYank: enterInsertMode + "y": -> @enterVisualMode yYanksLine: true + "d": -> @enterVisualMode deleteFromDocument: true, dYanksLine: true + "c": -> @enterVisualMode deleteFromDocument: true, onYank: => @enterInsertMode() - "D": => @enterVisualMode runMovement: "$", deleteFromDocument: true - "C": => @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: enterInsertMode + "D": -> @enterVisualMode runMovement: "$", deleteFromDocument: true + "C": -> @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() # Disabled as potentially confusing. # # If the input is empty, then enter insert mode immediately # unless @element.isContentEditable # if @element.value.trim() == "" - # enterInsertMode() + # @enterInsertMode() # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 enterVisualMode: (options = {}) -> defaults = badge: "" - underEditMode: true initialCount: @countPrefix oneMovementOnly: true - new VisualMode extend defaults, options @countPrefix = "" + @launchSubMode VisualMode, extend defaults, options + + enterInsertMode: () -> + @launchSubMode InsertMode, badge: "I", blurOnEscape: false + + launchSubMode: (mode, options = {}) -> + @lastSubMode = + mode: mode + instance: new mode extend options, editModeParent: @ pasteClipboard: (direction) -> @paste (text) => @@ -449,7 +469,7 @@ class EditMode extends Movement openLine: (direction) -> @runMovement "#{direction} lineboundary" - enterInsertMode() + @enterInsertMode() DomUtils.simulateTextEntry @element, "\n" @runMovement "backward character" if direction == backward @@ -471,35 +491,42 @@ class EditMode extends Movement locked = false exit: (event, target) -> - super() + super event, target + + lastSubMode = + if @lastSubMode?.instance.modeIsActive + @lastSubMode.instance.exit event, target + @lastSubMode + if event?.type == "keydown" and KeyboardUtils.isEscape event if target? and DomUtils.isDOMDescendant @element, target @element.blur() + if event?.type == "blur" - new BlurredEditMode @options + new SuspendedEditMode @options, lastSubMode # In edit mode, the input blurs if the user changes tabs or clicks outside of the element. In the former # case, the user expects to remain in edit mode. In the latter case, they may just be copying some text with -# the mouse/Ctrl-C, and again they expect to remain in edit mode when they return. BlurredEditMode monitors +# the mouse/Ctrl-C, and again they expect to remain in edit mode when they return. SuspendedEditMode monitors # various events and tries to either exit completely or re-enter edit mode as appropriate. -class BlurredEditMode extends Mode - constructor: (originalOptions) -> +class SuspendedEditMode extends Mode + constructor: (editModeOptions, lastSubMode = null) -> super - name: "blurred-edit" - singleton: originalOptions.singleton + name: "suspended-edit" + singleton: editModeOptions.singleton @push _name: "#{@id}/focus" focus: (event) => @alwaysContinueBubbling => - if event?.target == originalOptions.singleton + if event?.target == editModeOptions.singleton console.log "#{@id}: reactivating edit mode" if @debug - new EditMode originalOptions + editMode = new EditMode editModeOptions + editMode.launchSubMode lastSubMode.mode, lastSubMode.instance.options if lastSubMode keypress: (event) => @alwaysContinueBubbling => @exit() unless event.metaKey or event.ctrlKey or event.altKey - root = exports ? window root.VisualMode = VisualMode root.VisualLineMode = VisualLineMode -- cgit v1.2.3 From d57b08232513b285360b220dbb4de91bc6b61765 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 11:30:21 +0000 Subject: Visual/edit modes: fix problem resuming insert mode. --- content_scripts/mode_visual_edit.coffee | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 1271a7fe..ff9114e9 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -456,12 +456,14 @@ class EditMode extends Movement @launchSubMode VisualMode, extend defaults, options enterInsertMode: () -> - @launchSubMode InsertMode, badge: "I", blurOnEscape: false + @launchSubMode InsertMode, + exitOnEscape: true + targetElement: @options.targetElement launchSubMode: (mode, options = {}) -> @lastSubMode = mode: mode - instance: new mode extend options, editModeParent: @ + instance: Mode.cloneMode mode, extend options, editModeParent: @ pasteClipboard: (direction) -> @paste (text) => @@ -521,8 +523,9 @@ class SuspendedEditMode extends Mode @alwaysContinueBubbling => if event?.target == editModeOptions.singleton console.log "#{@id}: reactivating edit mode" if @debug - editMode = new EditMode editModeOptions - editMode.launchSubMode lastSubMode.mode, lastSubMode.instance.options if lastSubMode + editMode = Mode.cloneMode EditMode, editModeOptions + if lastSubMode + editMode.launchSubMode lastSubMode.mode, lastSubMode.instance.options keypress: (event) => @alwaysContinueBubbling => @exit() unless event.metaKey or event.ctrlKey or event.altKey -- cgit v1.2.3 From 48e5f84237c073d8ac96f02fb51528e726c1bcdd Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 11:42:39 +0000 Subject: Visual/edit modes: fix problem resuming entering visual line mode. --- content_scripts/mode_visual_edit.coffee | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index ff9114e9..03b883c6 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -262,19 +262,15 @@ class VisualMode extends Movement constructor: (options = {}) -> @selection = window.getSelection() - if options.initialRange - @selection.removeAllRanges() - @selection.addRange options.initialRange - else - switch @selection.type - when "None" - unless @establishInitialSelection() - HUD.showForDuration "Create a selection before entering visual mode.", 2500 - return - when "Caret" - # Try to start with a visible selection. - @moveInDirection(forward) or @moveInDirection backward unless options.editModeParent - @scrollIntoView() if @selection.type == "Range" + switch @selection.type + when "None" + unless @establishInitialSelection() + HUD.showForDuration "Create a selection before entering visual mode.", 2500 + return + when "Caret" + # Try to start with a visible selection. + @moveInDirection(forward) or @moveInDirection backward unless options.editModeParent + @scrollIntoView() if @selection.type == "Range" defaults = name: "visual" @@ -285,7 +281,7 @@ class VisualMode extends Movement super extend defaults, options extend @commands, - "V": -> new VisualLineMode initialRange: @selection.getRangeAt(0).cloneRange() + "V": -> new VisualLineMode "y": -> # Special case: "yy" (the first from edit mode, and now the second). @selectLexicalEntity "lineboundary" if @options.yYanksLine and @keyPressCount == 1 -- cgit v1.2.3 From 90491cb8d4c44b520f7464acf55c574412d1dc47 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 14:11:03 +0000 Subject: Visual/edit modes: minor improvements. --- content_scripts/mode_visual_edit.coffee | 65 ++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 26 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 03b883c6..c5d78b51 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,4 +1,8 @@ +# Todo: +# Fix word movement, particularly for "a word". +# Konami code? + # 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. class SuppressPrintable extends Mode @@ -280,28 +284,38 @@ class VisualMode extends Movement alterMethod: "extend" super extend defaults, options - extend @commands, - "V": -> new VisualLineMode - "y": -> - # Special case: "yy" (the first from edit mode, and now the second). - @selectLexicalEntity "lineboundary" if @options.yYanksLine and @keyPressCount == 1 - @yank() + unless @options.oneMovementOnly + extend @commands, + "V": -> new VisualLineMode + "y": -> @yank() + # Additional commands when run under edit mode. if @options.editModeParent and not @options.oneMovementOnly extend @commands, "c": -> @yank deleteFromDocument: true; @options.editModeParent.enterInsertMode() "x": -> @yank deleteFromDocument: true - "d": -> - # Special case: "dd" (the first from edit mode, and now the second). - @selectLexicalEntity "lineboundary" if @options.dYanksLine and @keyPressCount == 1 + "d": -> @yank deleteFromDocument: true + + # For "yy". + if @options.yYanksLine + @commands.y = -> + if @keyPressCount == 1 + @selectLexicalEntity "lineboundary" + @yank() + + # For "dd". + if @options.dYanksLine + @commands.d = -> + if @keyPressCount == 1 + @selectLexicalEntity "lineboundary" @yank deleteFromDocument: true + # For "daw", "das", "dap", "caw", "cas", "cap". if @options.oneMovementOnly - extend @commands, - "a": -> - if @keyPressCount == 1 - for entity in [ "word", "sentence", "paragraph" ] - do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity + @commands.a = -> + if @keyPressCount == 1 + for entity in [ "word", "sentence", "paragraph" ] + do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity unless @options.editModeParent @installFindMode() @@ -395,7 +409,7 @@ class VisualMode extends Movement class VisualLineMode extends VisualMode constructor: (options = {}) -> - options.name = "visual/line" + options.name ||= "visual/line" super options unless @selection?.type == "None" @selectLexicalEntity "lineboundary" @@ -427,14 +441,14 @@ class EditMode extends Movement "P": -> @pasteClipboard backward "v": -> @launchSubMode VisualMode - "Y": -> @enterVisualMode runMovement: "Y" - "x": -> @enterVisualMode runMovement: "h", deleteFromDocument: true - "y": -> @enterVisualMode yYanksLine: true - "d": -> @enterVisualMode deleteFromDocument: true, dYanksLine: true - "c": -> @enterVisualMode deleteFromDocument: true, onYank: => @enterInsertMode() + "Y": -> @enterVisualModeForMovement runMovement: "Y" + "x": -> @enterVisualModeForMovement runMovement: "h", deleteFromDocument: true + "y": -> @enterVisualModeForMovement yYanksLine: true + "d": -> @enterVisualModeForMovement deleteFromDocument: true, dYanksLine: true + "c": -> @enterVisualModeForMovement deleteFromDocument: true, onYank: => @enterInsertMode() - "D": -> @enterVisualMode runMovement: "$", deleteFromDocument: true - "C": -> @enterVisualMode runMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() + "D": -> @enterVisualModeForMovement runMovement: "$", deleteFromDocument: true + "C": -> @enterVisualModeForMovement runMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() # Disabled as potentially confusing. # # If the input is empty, then enter insert mode immediately @@ -443,13 +457,12 @@ class EditMode extends Movement # @enterInsertMode() # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 - enterVisualMode: (options = {}) -> - defaults = - badge: "" + enterVisualModeForMovement: (options = {}) -> + @launchSubMode VisualMode, extend options, + badge: "M" initialCount: @countPrefix oneMovementOnly: true @countPrefix = "" - @launchSubMode VisualMode, extend defaults, options enterInsertMode: () -> @launchSubMode InsertMode, -- cgit v1.2.3 From 190013f3c3b4e4c4adbd87b892589a4617141820 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 26 Jan 2015 18:31:23 +0000 Subject: Visual/edit modes: "P" and "p" to copy-and-go. --- content_scripts/mode_visual_edit.coffee | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index c5d78b51..6fadd5c8 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -2,6 +2,7 @@ # Todo: # Fix word movement, particularly for "a word". # Konami code? +# p/P. # 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. @@ -289,6 +290,16 @@ class VisualMode extends Movement "V": -> new VisualLineMode "y": -> @yank() + # "P" and "p" to copy-and-go (but not under edit mode). + unless @options.editModeParent + do => + yankAndOpenAsUrl = (handler) => + chrome.runtime.sendMessage handler: handler, url: @yank() + + extend @commands, + "p": -> yankAndOpenAsUrl "openUrlInCurrentTab" + "P": -> yankAndOpenAsUrl "openUrlInNewTab" + # Additional commands when run under edit mode. if @options.editModeParent and not @options.oneMovementOnly extend @commands, @@ -517,9 +528,9 @@ class EditMode extends Movement new SuspendedEditMode @options, lastSubMode # In edit mode, the input blurs if the user changes tabs or clicks outside of the element. In the former -# case, the user expects to remain in edit mode. In the latter case, they may just be copying some text with -# the mouse/Ctrl-C, and again they expect to remain in edit mode when they return. SuspendedEditMode monitors -# various events and tries to either exit completely or re-enter edit mode as appropriate. +# case, the user expects to remain in edit mode when they return. In the latter case, they may just be +# copying some text with the mouse/Ctrl-C, and again they expect to remain in edit mode. SuspendedEditMode +# monitors various events and tries to either exit completely or re-enter edit mode, as appropriate. class SuspendedEditMode extends Mode constructor: (editModeOptions, lastSubMode = null) -> super @@ -527,10 +538,10 @@ class SuspendedEditMode extends Mode singleton: editModeOptions.singleton @push - _name: "#{@id}/focus" + _name: "#{@id}/monitor" focus: (event) => @alwaysContinueBubbling => - if event?.target == editModeOptions.singleton + if event?.target == editModeOptions.targetElement console.log "#{@id}: reactivating edit mode" if @debug editMode = Mode.cloneMode EditMode, editModeOptions if lastSubMode -- cgit v1.2.3 From 8d33db411b556b37a8b0d54aba507440cfc51e0a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 27 Jan 2015 04:24:34 +0000 Subject: Visual/edit modes: minor changes. --- content_scripts/mode_visual_edit.coffee | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 6fadd5c8..73f154bb 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -2,10 +2,10 @@ # Todo: # Fix word movement, particularly for "a word". # Konami code? -# p/P. +# Use find as a mode. # 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. +# through chrome keyboard shortcuts. It's a keyboard-event backstop for all of the following modes. class SuppressPrintable extends Mode constructor: (options) -> handler = (event) => @@ -18,7 +18,7 @@ class SuppressPrintable extends Mode DomUtils.suppressPropagation @stopBubblingAndFalse else - false + @suppressEvent else @stopBubblingAndTrue @@ -65,13 +65,10 @@ class Movement extends MaintainCount opposite: { forward: backward, backward: forward } copy: (text) -> - chrome.runtime.sendMessage - handler: "copyToClipboard" - data: text + chrome.runtime.sendMessage handler: "copyToClipboard", data: text if text paste: (callback) -> - chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> - callback response + chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response # Swap the anchor node/offset and the focus node/offset. reverseSelection: -> @@ -375,21 +372,26 @@ class VisualMode extends Movement @protectClipboard => initialRange = @selection.getRangeAt(0).cloneRange() direction = @getDirection() - which = if direction == forward then "start" else "end" + # Start by re-selecting the previous match, if any. This tells Chrome where to start from. if previousFindRange @selection.removeAllRanges() @selection.addRange previousFindRange window.find query, caseSensitive, findBackwards, true, false, true, false previousFindRange = newFindRange = @selection.getRangeAt(0).cloneRange() + # FIXME(smblott). What if there were no matches? + # Now, install a range from the original selection to the new match. range = document.createRange() + which = if direction == forward then "start" else "end" range.setStart initialRange["#{which}Container"], initialRange["#{which}Offset"] range.setEnd newFindRange.endContainer, newFindRange.endOffset @selection.removeAllRanges() @selection.addRange range + # If we're going backwards (or if the election ended up empty), then extend the selection again, + # this time to include the match itself. if @getDirection() == backward or @selection.toString().length == 0 range.setStart newFindRange.startContainer, newFindRange.startOffset @selection.removeAllRanges() -- cgit v1.2.3 From 0f205f2c672773eef4fc5d7637195e3f6d7d75f6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 27 Jan 2015 05:30:13 +0000 Subject: Visual/edit modes: better word-movement/"w". --- content_scripts/mode_visual_edit.coffee | 41 +++++++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 9 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 73f154bb..48c5f47f 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -70,6 +70,31 @@ class Movement extends MaintainCount paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response + # Return a value which changes whenever the selection changes, regardless of whether the selection is + # collapsed or not. + hashSelection: -> + [ @element?.selectionStart, @selection.toString().length ].join "/" + + # Call a function. Return true if the selection changed as a side effect, false otherwise. + selectionChanged: (func) -> + before = @hashSelection() + func() + console.log before, @hashSelection() + @hashSelection() != before + + # Run a movement. The single movement argument can be a string of the form "direction amount", e.g. + # "forward word", or a list, e.g. [ "forward", "word" ]. + runMovement: (movement) -> + movement = movement.split(" ") if typeof movement == "string" + console.log movement.join " " + @selection.modify @alterMethod, movement... + + # Run a sequence of movements, stopping if a movement fails to change the selection. + runMovements: (movements...) -> + for movement in movements + return false unless @selectionChanged => @runMovement movement + true + # Swap the anchor node/offset and the focus node/offset. reverseSelection: -> element = document.activeElement @@ -91,12 +116,6 @@ class Movement extends MaintainCount which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Run a movement command. The single movement argument can be a string of the form "direction amount", e.g. - # "forward word", or a list, e.g. [ "forward", "word" ]. - runMovement: (movement) -> - movement = movement.split(" ") if typeof movement == "string" - @selection.modify @alterMethod, movement... - # 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) -> @@ -115,9 +134,13 @@ class Movement extends MaintainCount return if 0 < success then direction else @opposite[direction] backward - # An approximation of the vim "w" movement. - moveForwardWord: (direction) -> - @runMovement movement for movement in [ "forward word", "forward word", "backward word" ] + # An approximation of the vim "w" movement; only ever used in the forward direction. The extra character + # movements at the end allow us to also get to the end of the very-last word. + moveForwardWord: () -> + # First, move to the end of the preceding word... + if @runMovements "forward character", "backward word", "forward word" + # And then to the start of the following word... + @runMovements "forward word", "forward character", "backward character", "backward word" collapseSelection: -> if 0 < @selection.toString().length -- cgit v1.2.3 From 8e9c554d81df415632a8c995a8a8789e0985d0e6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 27 Jan 2015 07:01:27 +0000 Subject: Visual/edit modes: miscellaneous changes. - Various argument forms for runMovement. - Better visual line mode line selection. - Include preceding white space for "daw" and friends. - Vim-like handling of "2d3w" - count is six". --- content_scripts/mode_visual_edit.coffee | 176 ++++++++++++++++---------------- 1 file changed, 88 insertions(+), 88 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 48c5f47f..e98fe5f2 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,13 +1,13 @@ # Todo: -# Fix word movement, particularly for "a word". # Konami code? # Use find as a mode. +# Refactor visual/movement modes. -# This prevents printable characters from being passed through to underlying page. It should, however, allow -# through chrome keyboard shortcuts. It's a keyboard-event backstop for all of the following modes. +# This prevents printable characters from being passed through to the underlying page. It should, however, +# allow through chrome keyboard shortcuts. It's a keyboard-event backstop for visual mode and edit mode. class SuppressPrintable extends Mode - constructor: (options) -> + constructor: (options = {}) -> handler = (event) => if KeyboardUtils.isPrintable event if event.type == "keydown" @@ -22,23 +22,20 @@ class SuppressPrintable extends Mode else @stopBubblingAndTrue - # This is pushed onto the handler stack before calling super(). Therefore, it ends up underneath (or - # after) all of the other handlers associated with the mode. - handlerId = handlerStack.push - _name: "#{@id}/suppress-printable" + super extend options, keydown: handler keypress: handler keyup: handler - super options - @handlers.push handlerId - # This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. class MaintainCount extends SuppressPrintable constructor: (options) -> - @countPrefix = options.initialCount || "" super options + @countPrefix = "" + @countPrefixFactor = 1 + @countPrefixFactor = @getCountPrefix options.initialCountPrefix if options.initialCountPrefix + @push _name: "#{@id}/maintain-count" keypress: (event) => @@ -47,14 +44,19 @@ class MaintainCount extends SuppressPrintable keyChar = String.fromCharCode event.charCode @countPrefix = if keyChar?.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0" - if @options.initialCount - @countPrefix = "" - delete @options.initialCount @countPrefix + keyChar else "" -# Some symbolic names. + # This handles both "d3w" and "3dw". Also, "3d2w" deletes six words. + getCountPrefix: (prefix = @countPrefix) -> + prefix = prefix.toString() if typeof prefix == "number" + count = @countPrefixFactor * if 0 < prefix?.length then parseInt prefix else 1 + @countPrefix = "" + @countPrefixFactor = 1 + count + +# Some symbolic names for widely-used strings. forward = "forward" backward = "backward" character = "character" @@ -62,7 +64,7 @@ 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 copy: (text) -> chrome.runtime.sendMessage handler: "copyToClipboard", data: text if text @@ -71,22 +73,24 @@ class Movement extends MaintainCount chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response # Return a value which changes whenever the selection changes, regardless of whether the selection is - # collapsed or not. + # collapsed. hashSelection: -> [ @element?.selectionStart, @selection.toString().length ].join "/" - # Call a function. Return true if the selection changed as a side effect, false otherwise. + # Call a function; return true if the selection changed. selectionChanged: (func) -> - before = @hashSelection() - func() - console.log before, @hashSelection() - @hashSelection() != before - - # Run a movement. The single movement argument can be a string of the form "direction amount", e.g. - # "forward word", or a list, e.g. [ "forward", "word" ]. - runMovement: (movement) -> - movement = movement.split(" ") if typeof movement == "string" - console.log movement.join " " + before = @hashSelection(); func(); @hashSelection() != before + + # Run a movement. The arguments can be one of the following forms: + # - "forward word" (one argument, a string) + # - [ "forward", "word" ] (one argument, not a string) + # - "forward", "word" (two arguments) + runMovement: (args...) -> + movement = + if typeof(args[0]) == "string" and args.length == 1 + args[0].trim().split /\s+/ + else + if args.length == 1 then args[0] else args[...2] @selection.modify @alterMethod, movement... # Run a sequence of movements, stopping if a movement fails to change the selection. @@ -98,13 +102,13 @@ class Movement extends MaintainCount # Swap the anchor node/offset and the focus node/offset. reverseSelection: -> element = document.activeElement - if element and DomUtils.isEditable(element) and not element. isContentEditable + if element and DomUtils.isEditable(element) and not element.isContentEditable # Note(smblott). This implementation is unacceptably inefficient if the selection is large. We only use - # it if we have to. However, the normal method does not work for input elements. + # it if we have to. However, the normal method (below) does not work for input elements. direction = @getDirection() length = @selection.toString().length - @selection[if direction == forward then "collapseToEnd" else "collapseToStart"]() - @selection.modify "extend", @opposite[direction], character for [0...length] + @collapseSelectionToFocus() + @runMovement @opposite[direction], character for [0...length] else # Normal method. direction = @getDirection() @@ -116,9 +120,9 @@ class Movement extends MaintainCount which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # 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) -> + # Try to extend the selection one character in "direction". Return 1, -1 or 0, indicating whether the + # selection got bigger, or smaller, or is unchanged. + extendByOneCharacter: (direction) -> length = @selection.toString().length @selection.modify "extend", direction, character @selection.toString().length - length @@ -129,23 +133,27 @@ class Movement extends MaintainCount getDirection: -> # Try to move the selection forward or backward, check whether it got bigger or smaller (then restore it). for direction in [ forward, backward ] - if success = @moveInDirection direction - @moveInDirection @opposite[direction] - return if 0 < success then direction else @opposite[direction] - backward + if change = @extendByOneCharacter direction + @extendByOneCharacter @opposite[direction] + return if 0 < change then direction else @opposite[direction] + forward - # An approximation of the vim "w" movement; only ever used in the forward direction. The extra character - # movements at the end allow us to also get to the end of the very-last word. + # An approximation of the vim "w" movement; only ever used in the forward direction. The last two character + # movements allow us to also get to the end of the very-last word. moveForwardWord: () -> # First, move to the end of the preceding word... if @runMovements "forward character", "backward word", "forward word" # And then to the start of the following word... @runMovements "forward word", "forward character", "backward character", "backward word" - collapseSelection: -> + collapseSelectionToAnchor: -> if 0 < @selection.toString().length @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() + collapseSelectionToFocus: -> + if 0 < @selection.toString().length + @selection[if @getDirection() == forward then "collapseToEnd" else "collapseToStart"]() + movements: "l": "forward character" "h": "backward character" @@ -169,9 +177,8 @@ class Movement extends MaintainCount @selection = window.getSelection() @movements = extend {}, @movements @commands = {} - @alterMethod = options.alterMethod || "extend" @keyQueue = "" - @keyPressCount = 0 + @keypressCount = 0 @yankedText = "" super options @@ -179,16 +186,16 @@ class Movement extends MaintainCount @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 + if @options.singleMovementOnly + # This instance has been created just to run a single movement only and then yank the result. + @handleMovementKeyChar @options.singleMovementOnly @yank() return @push _name: "#{@id}/keypress" keypress: (event) => - @keyPressCount += 1 + @keypressCount += 1 unless event.metaKey or event.ctrlKey or event.altKey @keyQueue += String.fromCharCode event.charCode # We allow at most three characters for a command or movement mapping. @@ -207,20 +214,14 @@ class Movement extends MaintainCount else if @movements[command] @handleMovementKeyChar command - - if @options.oneMovementOnly - @yank() - return @suppressEvent - - break + break unless @options.oneMovementOnly + @yank() + return @suppressEvent @continueBubbling handleMovementKeyChar: (keyChar) -> - # 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 = "" + count = @getCountPrefix() if @movements[keyChar] @protectClipboard => for [0...count] @@ -229,14 +230,14 @@ class Movement extends MaintainCount when "function" then @movements[keyChar].call @ @scrollIntoView() - # Yank the selection. Always exits. Returns the yanked text. + # Yank the selection; always exits; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if args.deleteFromDocument or @options.deleteFromDocument console.log "yank:", @yankedText if @debug message = @yankedText.replace /\s+/g, " " - length = message.length + length = @yankedText.length message = message[...12] + "..." if 15 < length plural = if length == 1 then "" else "s" HUD.showForDuration "Yanked #{length} character#{plural}: \"#{message}\".", 2500 @@ -245,12 +246,14 @@ class Movement extends MaintainCount @exit() @yankedText - # Select a lexical entity, such as a word, a line, or a sentence. The argument should be a movement target, - # such as "word" or "lineboundary". + # Select a lexical entity, such as a word, a line, or a sentence. The entity should be a Chrome movement + # type, such as "word" or "lineboundary". This assumes that the selection is initially collapsed. selectLexicalEntity: (entity) -> - for direction in [ backward, forward ] - @reverseSelection() - @runMovement [ direction, entity ] + @runMovement forward, entity + @selection.collapseToEnd() + @runMovement backward, entity + # Move the end of the preceding entity. + @runMovements [ backward, entity ], [ forward, entity ] # Try to scroll the focus into view. scrollIntoView: -> @@ -270,7 +273,7 @@ class Movement extends MaintainCount # 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. It seems to work most (but not all) of the time. + # view. getElementWithFocus: (selection) -> r = t = selection.getRangeAt 0 if selection.type == "Range" @@ -286,6 +289,7 @@ class Movement extends MaintainCount class VisualMode extends Movement constructor: (options = {}) -> @selection = window.getSelection() + @alterMethod = "extend" switch @selection.type when "None" @@ -294,7 +298,7 @@ class VisualMode extends Movement return when "Caret" # Try to start with a visible selection. - @moveInDirection(forward) or @moveInDirection backward unless options.editModeParent + @extendByOneCharacter(forward) or @extendByOneCharacter backward unless options.editModeParent @scrollIntoView() if @selection.type == "Range" defaults = @@ -302,7 +306,6 @@ class VisualMode extends Movement badge: "V" singleton: VisualMode exitOnEscape: true - alterMethod: "extend" super extend defaults, options unless @options.oneMovementOnly @@ -330,21 +333,21 @@ class VisualMode extends Movement # For "yy". if @options.yYanksLine @commands.y = -> - if @keyPressCount == 1 + if @keypressCount == 1 @selectLexicalEntity "lineboundary" @yank() # For "dd". if @options.dYanksLine @commands.d = -> - if @keyPressCount == 1 + if @keypressCount == 1 @selectLexicalEntity "lineboundary" @yank deleteFromDocument: true # For "daw", "das", "dap", "caw", "cas", "cap". if @options.oneMovementOnly @commands.a = -> - if @keyPressCount == 1 + if @keypressCount == 1 for entity in [ "word", "sentence", "paragraph" ] do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity @@ -366,12 +369,7 @@ class VisualMode extends Movement super @clipboardContents = text exit: (event, target) -> - if @options.editModeParent - if event?.type == "keydown" and KeyboardUtils.isEscape event - # Return to a caret for edit mode. - @collapseSelection() - - @collapseSelection() if @yankedText + @collapseSelectionToAnchor() if @yankedText or @options.editModeParent unless @options.editModeParent # Don't leave the user in insert mode just because they happen to have selected text within an input @@ -448,15 +446,19 @@ class VisualLineMode extends VisualMode options.name ||= "visual/line" super options unless @selection?.type == "None" - @selectLexicalEntity "lineboundary" + initialDirection = @getDirection() + for direction in [ initialDirection, @opposite[initialDirection] ] + @runMovement direction, "lineboundary" + @reverseSelection() handleMovementKeyChar: (keyChar) -> super keyChar - @runMovement "#{@getDirection()} lineboundary", true + @runMovement @getDirection(), "lineboundary" class EditMode extends Movement constructor: (options = {}) -> @element = document.activeElement + @alterMethod = "move" return unless @element and DomUtils.isEditable @element defaults = @@ -464,7 +466,6 @@ class EditMode extends Movement badge: "E" exitOnEscape: true exitOnBlur: @element - alterMethod: "move" super extend defaults, options extend @commands, @@ -477,14 +478,14 @@ class EditMode extends Movement "P": -> @pasteClipboard backward "v": -> @launchSubMode VisualMode - "Y": -> @enterVisualModeForMovement runMovement: "Y" - "x": -> @enterVisualModeForMovement runMovement: "h", deleteFromDocument: true + "Y": -> @enterVisualModeForMovement singleMovementOnly: "Y" + "x": -> @enterVisualModeForMovement singleMovementOnly: "h", deleteFromDocument: true "y": -> @enterVisualModeForMovement yYanksLine: true "d": -> @enterVisualModeForMovement deleteFromDocument: true, dYanksLine: true "c": -> @enterVisualModeForMovement deleteFromDocument: true, onYank: => @enterInsertMode() - "D": -> @enterVisualModeForMovement runMovement: "$", deleteFromDocument: true - "C": -> @enterVisualModeForMovement runMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() + "D": -> @enterVisualModeForMovement singleMovementOnly: "$", deleteFromDocument: true + "C": -> @enterVisualModeForMovement singleMovementOnly: "$", deleteFromDocument: true, onYank: => @enterInsertMode() # Disabled as potentially confusing. # # If the input is empty, then enter insert mode immediately @@ -496,9 +497,8 @@ class EditMode extends Movement enterVisualModeForMovement: (options = {}) -> @launchSubMode VisualMode, extend options, badge: "M" - initialCount: @countPrefix + initialCountPrefix: @getCountPrefix() oneMovementOnly: true - @countPrefix = "" enterInsertMode: () -> @launchSubMode InsertMode, @@ -515,7 +515,7 @@ class EditMode extends Movement DomUtils.simulateTextEntry @element, text if text openLine: (direction) -> - @runMovement "#{direction} lineboundary" + @runMovement direction, "lineboundary" @enterInsertMode() DomUtils.simulateTextEntry @element, "\n" @runMovement "backward character" if direction == backward -- cgit v1.2.3 From 9d97ce8dab7672d7d1846f7cbe4d22af80c91b01 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 27 Jan 2015 11:32:53 +0000 Subject: Visual/edit modes: self code review. --- content_scripts/mode_visual_edit.coffee | 274 +++++++++++++++----------------- 1 file changed, 131 insertions(+), 143 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index e98fe5f2..a1666d75 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -115,8 +115,7 @@ class Movement extends MaintainCount original = @selection.getRangeAt(0).cloneRange() range = original.cloneRange() range.collapse direction == backward - @selection.removeAllRanges() - @selection.addRange range + @selectRange range which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] @@ -154,6 +153,10 @@ class Movement extends MaintainCount if 0 < @selection.toString().length @selection[if @getDirection() == forward then "collapseToEnd" else "collapseToStart"]() + selectRange: (range) -> + @selection.removeAllRanges() + @selection.addRange range + movements: "l": "forward character" "h": "backward character" @@ -186,9 +189,9 @@ class Movement extends MaintainCount @movements.B = @movements.b @movements.W = @movements.w - if @options.singleMovementOnly + if @options.immediateMovement # This instance has been created just to run a single movement only and then yank the result. - @handleMovementKeyChar @options.singleMovementOnly + @handleMovementKeyChar @options.immediateMovement @yank() return @@ -198,37 +201,33 @@ class Movement extends MaintainCount @keypressCount += 1 unless event.metaKey or event.ctrlKey or event.altKey @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 (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] + # Keep at most two characters in the key queue. + @keyQueue = @keyQueue.slice Math.max 0, @keyQueue.length - 2 + for command in [ @keyQueue, @keyQueue[1..] ] + if command and (@movements[command] or @commands[command]) @selection = window.getSelection() @keyQueue = "" if @commands[command] - @commands[command].call @ + @commands[command].call @, @getCountPrefix() @scrollIntoView() return @suppressEvent else if @movements[command] - @handleMovementKeyChar command - break unless @options.oneMovementOnly - @yank() + @handleMovementKeyChar command, @getCountPrefix() + @yank() if @options.oneMovementOnly return @suppressEvent @continueBubbling - handleMovementKeyChar: (keyChar) -> - count = @getCountPrefix() - if @movements[keyChar] - @protectClipboard => - for [0...count] - switch typeof @movements[keyChar] - when "string" then @runMovement @movements[keyChar] - when "function" then @movements[keyChar].call @ - @scrollIntoView() + handleMovementKeyChar: (keyChar, count = 1) -> + action = + switch typeof @movements[keyChar] + when "string" then => @runMovement @movements[keyChar] + when "function" then => @movements[keyChar].call @ + @protectClipboard => + action() for [0...count] + @scrollIntoView() # Yank the selection; always exits; returns the yanked text. yank: (args = {}) -> @@ -308,59 +307,51 @@ class VisualMode extends Movement exitOnEscape: true super extend defaults, options + # Additional commands when not being run only for movement. unless @options.oneMovementOnly - extend @commands, - "V": -> new VisualLineMode - "y": -> @yank() - - # "P" and "p" to copy-and-go (but not under edit mode). - unless @options.editModeParent - do => - yankAndOpenAsUrl = (handler) => - chrome.runtime.sendMessage handler: handler, url: @yank() - - extend @commands, - "p": -> yankAndOpenAsUrl "openUrlInCurrentTab" - "P": -> yankAndOpenAsUrl "openUrlInNewTab" + @commands.y = -> @yank() + @commands.V = -> new VisualLineMode + @commands.p = -> chrome.runtime.sendMessage handler: "openUrlInCurrentTab", url: @yank() + @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() - # Additional commands when run under edit mode. + # Additional commands when run under edit mode (but not just for movement). if @options.editModeParent and not @options.oneMovementOnly - extend @commands, - "c": -> @yank deleteFromDocument: true; @options.editModeParent.enterInsertMode() - "x": -> @yank deleteFromDocument: true - "d": -> @yank deleteFromDocument: true - - # For "yy". - if @options.yYanksLine - @commands.y = -> - if @keypressCount == 1 - @selectLexicalEntity "lineboundary" - @yank() + @commands.x = -> @yank deleteFromDocument: true + @commands.d = -> @yank deleteFromDocument: true + @commands.c = -> + @yank deleteFromDocument: true + @options.editModeParent.enterInsertMode() - # For "dd". - if @options.dYanksLine - @commands.d = -> + # For "yy" and "dd". + if @options.yankLineCharacter + @commands[@options.yankLineCharacter] = -> if @keypressCount == 1 @selectLexicalEntity "lineboundary" - @yank deleteFromDocument: true + @yank() - # For "daw", "das", "dap", "caw", "cas", "cap". + # For "daw", "cas", and so on. if @options.oneMovementOnly @commands.a = -> if @keypressCount == 1 for entity in [ "word", "sentence", "paragraph" ] - do (entity) => @movements[entity.charAt 0] = -> @selectLexicalEntity entity + do (entity) => + @movements[entity.charAt 0] = -> + if @keypressCount == 2 + @selectLexicalEntity entity + @yank() unless @options.editModeParent @installFindMode() - # Grab the initial clipboard contents. We'll try to keep them intact until we get an explicit yank. + # Grab the initial clipboard contents. We try to keep them intact until we get an explicit yank. @clipboardContents = "" @paste (text) => @clipboardContents = text if text # # End of VisualMode constructor. + # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We + # always reinstall the original clipboard contents when we're done. protectClipboard: (func) -> func() @copy @clipboardContents if @clipboardContents @@ -378,11 +369,9 @@ class VisualMode extends Movement document.activeElement.blur() super event, target - # Copying the yanked text to the clipboard must be the very last thing we do, because other operations - # (like collapsing the selection) interfere with the clipboard. @copy @yankedText if @yankedText - + # FIXME(smblott). This is a mess, it needs to be reworked. Ideally, incorporate FindMode. installFindMode: -> previousFindRange = null @@ -394,66 +383,62 @@ class VisualMode extends Movement initialRange = @selection.getRangeAt(0).cloneRange() direction = @getDirection() - # Start by re-selecting the previous match, if any. This tells Chrome where to start from. - if previousFindRange - @selection.removeAllRanges() - @selection.addRange previousFindRange + # Re-selecting the previous match, if any; this tells Chrome where to start. + @selectRange previousFindRange if previousFindRange window.find query, caseSensitive, findBackwards, true, false, true, false previousFindRange = newFindRange = @selection.getRangeAt(0).cloneRange() - # FIXME(smblott). What if there were no matches? + # FIXME(smblott). What if there are no matches? - # Now, install a range from the original selection to the new match. + # Install a new range from the original selection anchor to end of the new match. range = document.createRange() which = if direction == forward then "start" else "end" range.setStart initialRange["#{which}Container"], initialRange["#{which}Offset"] range.setEnd newFindRange.endContainer, newFindRange.endOffset - @selection.removeAllRanges() - @selection.addRange range + @selectRange range - # If we're going backwards (or if the election ended up empty), then extend the selection again, - # this time to include the match itself. + # If we're now going backwards (or if the selection is empty), then extend the selection to include + # the match itself. if @getDirection() == backward or @selection.toString().length == 0 range.setStart newFindRange.startContainer, newFindRange.startOffset - @selection.removeAllRanges() - @selection.addRange range + @selectRange range - extend @movements, - "n": -> executeFind false - "N": -> executeFind true + @movements.n = -> executeFind false + @movements.N = -> executeFind true + # When visual mode starts and there's no existing selection, we try to establish one. 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. establishInitialSelection: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT while node = nodes.nextNode() - # Try not to pick really small nodes. They're likely to be part of a banner. + # Don't pick really short texts; they're likely to be part of a banner. if node.nodeType == 3 and 50 <= node.data.trim().length element = node.parentElement if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element + offset = node.data.length - node.data.replace(/^\s+/, "").length range = document.createRange() - text = node.data - trimmed = text.replace /^\s+/, "" - offset = text.length - trimmed.length range.setStart node, offset range.setEnd node, offset + 1 - @selection.removeAllRanges() - @selection.addRange range + @selectRange range @scrollIntoView() return true false class VisualLineMode extends VisualMode constructor: (options = {}) -> - options.name ||= "visual/line" - super options - unless @selection?.type == "None" - initialDirection = @getDirection() - for direction in [ initialDirection, @opposite[initialDirection] ] - @runMovement direction, "lineboundary" - @reverseSelection() + super extend { name: "visual/line" }, options + @extendSelection() handleMovementKeyChar: (keyChar) -> super keyChar - @runMovement @getDirection(), "lineboundary" + @extendSelection() + + extendSelection: -> + initialDirection = @getDirection() + for direction in [ initialDirection, @opposite[initialDirection] ] + @runMovement direction, "lineboundary" + @reverseSelection() class EditMode extends Movement constructor: (options = {}) -> @@ -469,30 +454,31 @@ class EditMode extends Movement super extend defaults, options extend @commands, - "i": -> @enterInsertMode() - "a": -> @enterInsertMode() - "A": -> @runMovement "forward lineboundary"; @enterInsertMode() - "o": -> @openLine forward - "O": -> @openLine backward - "p": -> @pasteClipboard forward - "P": -> @pasteClipboard backward - "v": -> @launchSubMode VisualMode - - "Y": -> @enterVisualModeForMovement singleMovementOnly: "Y" - "x": -> @enterVisualModeForMovement singleMovementOnly: "h", deleteFromDocument: true - "y": -> @enterVisualModeForMovement yYanksLine: true - "d": -> @enterVisualModeForMovement deleteFromDocument: true, dYanksLine: true - "c": -> @enterVisualModeForMovement deleteFromDocument: true, onYank: => @enterInsertMode() - - "D": -> @enterVisualModeForMovement singleMovementOnly: "$", deleteFromDocument: true - "C": -> @enterVisualModeForMovement singleMovementOnly: "$", deleteFromDocument: true, onYank: => @enterInsertMode() - - # Disabled as potentially confusing. - # # If the input is empty, then enter insert mode immediately - # unless @element.isContentEditable - # if @element.value.trim() == "" - # @enterInsertMode() - # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 + i: -> @enterInsertMode() + a: -> @enterInsertMode() + A: -> @runMovement "forward lineboundary"; @enterInsertMode() + o: -> @openLine forward + O: -> @openLine backward + p: -> @pasteClipboard forward + P: -> @pasteClipboard backward + v: -> @launchSubMode VisualMode + + Y: -> @enterVisualModeForMovement immediateMovement: "Y" + x: -> @enterVisualModeForMovement immediateMovement: "h", deleteFromDocument: true + X: -> @enterVisualModeForMovement immediateMovement: "l", deleteFromDocument: true + y: -> @enterVisualModeForMovement yankLineCharacter: "y" + d: -> @enterVisualModeForMovement yankLineCharacter: "d", deleteFromDocument: true + c: -> @enterVisualModeForMovement deleteFromDocument: true, onYank: => @enterInsertMode() + + D: -> @enterVisualModeForMovement immediateMovement: "$", deleteFromDocument: true + C: -> @enterVisualModeForMovement immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() + + # Disabled as potentially confusing. + # # If the input is empty, then enter insert mode immediately. + # unless @element.isContentEditable + # if @element.value.trim() == "" + # @enterInsertMode() + # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 enterVisualModeForMovement: (options = {}) -> @launchSubMode VisualMode, extend options, @@ -508,7 +494,7 @@ class EditMode extends Movement launchSubMode: (mode, options = {}) -> @lastSubMode = mode: mode - instance: Mode.cloneMode mode, extend options, editModeParent: @ + instance: new mode extend options, editModeParent: @ pasteClipboard: (direction) -> @paste (text) => @@ -518,13 +504,14 @@ class EditMode extends Movement @runMovement direction, "lineboundary" @enterInsertMode() DomUtils.simulateTextEntry @element, "\n" - @runMovement "backward character" if direction == backward + @runMovement backward, character if direction == backward - # Backup the clipboard, then call a function (which may affect the selection text, and hence the - # clipboard too), then restore the clipboard. + # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We + # always reinstall the original clipboard contents when we're done. Note, this may be asynchronous. We do + # this this way (as opposed to the simpler, synchronous method used by Visual mode) because the user may + # wish to select text with the mouse (while edit mode is active) to later paste with "p" or "P". protectClipboard: do -> locked = false - clipboard = "" (func) -> if locked @@ -532,15 +519,14 @@ class EditMode extends Movement else locked = true @paste (text) => - clipboard = text func() - @copy clipboard + @copy text locked = false exit: (event, target) -> super event, target - lastSubMode = + @lastSubMode = if @lastSubMode?.instance.modeIsActive @lastSubMode.instance.exit event, target @lastSubMode @@ -550,30 +536,32 @@ class EditMode extends Movement @element.blur() if event?.type == "blur" - new SuspendedEditMode @options, lastSubMode - -# In edit mode, the input blurs if the user changes tabs or clicks outside of the element. In the former -# case, the user expects to remain in edit mode when they return. In the latter case, they may just be -# copying some text with the mouse/Ctrl-C, and again they expect to remain in edit mode. SuspendedEditMode -# monitors various events and tries to either exit completely or re-enter edit mode, as appropriate. -class SuspendedEditMode extends Mode - constructor: (editModeOptions, lastSubMode = null) -> - super - name: "suspended-edit" - singleton: editModeOptions.singleton - - @push - _name: "#{@id}/monitor" - focus: (event) => - @alwaysContinueBubbling => - if event?.target == editModeOptions.targetElement - console.log "#{@id}: reactivating edit mode" if @debug - editMode = Mode.cloneMode EditMode, editModeOptions - if lastSubMode - editMode.launchSubMode lastSubMode.mode, lastSubMode.instance.options - keypress: (event) => - @alwaysContinueBubbling => - @exit() unless event.metaKey or event.ctrlKey or event.altKey + # 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 tabs. 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 indefinately. 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 a newly-activated 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. + # + suspendedEditmode = new Mode + name: "#{@id}-suspended" + singleton: @options.singleton + + suspendedEditmode.push + _name: "suspended-edit/#{@id}/focus" + focus: (event) => + @alwaysContinueBubbling => + if event?.target == @options.targetElement + console.log "#{@id}: reactivating edit mode" if @debug + editMode = new EditMode @getConfigurationOptions() + if @lastSubMode + editMode.launchSubMode @lastSubMode.mode, @lastSubMode.instance.getConfigurationOptions() root = exports ? window root.VisualMode = VisualMode -- cgit v1.2.3 From a982ce074d517eb9e56c517d86a4bfb3869cf171 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 27 Jan 2015 16:36:49 +0000 Subject: Visual/edit modes: minor changes. - Minor changes. - Deactivate modes on inputs from focusInput(). --- content_scripts/mode_visual_edit.coffee | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index a1666d75..70d95dff 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -2,10 +2,11 @@ # Todo: # Konami code? # Use find as a mode. -# Refactor visual/movement modes. +# Perhaps refactor visual/movement modes. +# FocusInput selector is currently broken. # This prevents printable characters from being passed through to the underlying page. It should, however, -# allow through chrome keyboard shortcuts. It's a keyboard-event backstop for visual mode and edit mode. +# allow through Chrome keyboard shortcuts. It's a keyboard-event backstop for visual mode and edit mode. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => @@ -28,7 +29,7 @@ class SuppressPrintable extends Mode keyup: handler # This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. -class MaintainCount extends SuppressPrintable +class CountPrefix extends SuppressPrintable constructor: (options) -> super options @@ -61,9 +62,9 @@ forward = "forward" backward = "backward" character = "character" -# This implements movement commands with count prefixes (using MaintainCount) for both visual mode and edit +# This implements movement commands with count prefixes (using CountPrefix) for both visual mode and edit # mode. -class Movement extends MaintainCount +class Movement extends CountPrefix opposite: forward: backward, backward: forward copy: (text) -> -- cgit v1.2.3 From c117ac90faf8c685b40195ac54b237dfcb2b648b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 28 Jan 2015 05:02:29 +0000 Subject: Visual/edit modes: minor changes... - Minor refactoring. - Better selection of entity for "daw" an friends. - dd uses count, and works for empty lines. - Count for daw, etc. - Fix bug whereby selection cleared when changing tabs. --- content_scripts/mode_visual_edit.coffee | 128 ++++++++++++++++---------------- 1 file changed, 66 insertions(+), 62 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 70d95dff..d7188c1f 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -3,32 +3,27 @@ # Konami code? # Use find as a mode. # Perhaps refactor visual/movement modes. -# FocusInput selector is currently broken. +# Exit on Ctrl-Enter. +# Scroll is broken (again). Seems to be after dd. # This prevents printable characters from being passed through to the underlying page. It should, however, # allow through Chrome keyboard shortcuts. It's a keyboard-event backstop for visual mode and edit mode. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => - if KeyboardUtils.isPrintable event - if event.type == "keydown" - # Completely suppress Backspace and Delete. - if event.keyCode in [ 8, 46 ] - @suppressEvent - else - DomUtils.suppressPropagation - @stopBubblingAndFalse - else - @suppressEvent - else - @stopBubblingAndTrue + return @stopBubblingAndTrue unless KeyboardUtils.isPrintable event + return @suppressEvent unless event.type == "keydown" + # Completely suppress Backspace and Delete. + return @suppressEvent if event.keyCode in [ 8, 46 ] + DomUtils.suppressPropagation event + @stopBubblingAndFalse super extend options, keydown: handler keypress: handler keyup: handler -# This watches keyboard events and maintains @countPrefix as number keys and other keys are pressed. +# This watches keypresses and maintains the count prefix as number keys and other keys are pressed. class CountPrefix extends SuppressPrintable constructor: (options) -> super options @@ -57,13 +52,12 @@ class CountPrefix extends SuppressPrintable @countPrefixFactor = 1 count -# Some symbolic names for widely-used strings. +# Some symbolic names for frequently-used strings. forward = "forward" backward = "backward" character = "character" -# This implements movement commands with count prefixes (using CountPrefix) for both visual mode and edit -# mode. +# This implements movement commands with count prefixes for both visual mode and edit mode. class Movement extends CountPrefix opposite: forward: backward, backward: forward @@ -73,8 +67,7 @@ class Movement extends CountPrefix paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response - # Return a value which changes whenever the selection changes, regardless of whether the selection is - # collapsed. + # Return a value which changes whenever the selection changes. hashSelection: -> [ @element?.selectionStart, @selection.toString().length ].join "/" @@ -82,10 +75,10 @@ class Movement extends CountPrefix selectionChanged: (func) -> before = @hashSelection(); func(); @hashSelection() != before - # Run a movement. The arguments can be one of the following forms: - # - "forward word" (one argument, a string) - # - [ "forward", "word" ] (one argument, not a string) - # - "forward", "word" (two arguments) + # Run a movement. For convenience, the following three forms can be used: + # @runMovement "forward word" + # @runMovement [ "forward", "word" ] + # @runMovement "forward", "word" runMovement: (args...) -> movement = if typeof(args[0]) == "string" and args.length == 1 @@ -103,16 +96,15 @@ class Movement extends CountPrefix # Swap the anchor node/offset and the focus node/offset. reverseSelection: -> element = document.activeElement + direction = @getDirection() if element and DomUtils.isEditable(element) and not element.isContentEditable # Note(smblott). This implementation is unacceptably inefficient if the selection is large. We only use - # it if we have to. However, the normal method (below) does not work for input elements. - direction = @getDirection() + # it if we have to. However, 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. - direction = @getDirection() original = @selection.getRangeAt(0).cloneRange() range = original.cloneRange() range.collapse direction == backward @@ -138,12 +130,21 @@ class Movement extends CountPrefix return if 0 < change then direction else @opposite[direction] forward - # An approximation of the vim "w" movement; only ever used in the forward direction. The last two character - # movements allow us to also get to the end of the very-last word. - moveForwardWord: () -> + # An approximation of the vim "w" movement; only ever used in the forward direction. + moveForwardWord: -> + # First, move to the start of the current word... + @runMovement forward, character + @runMovement backward, "word" + # And then to the start of the next word... + @selectLexicalEntity "word" + return + # Previous version... + # This works in normal text inputs, but not in some contentEditable elements (notably the compose window + # on Google's Inbox). # First, move to the end of the preceding word... if @runMovements "forward character", "backward word", "forward word" # And then to the start of the following word... + # The two character movements allow us to also get to the end of the very-last word. @runMovements "forward word", "forward character", "backward character", "backward word" collapseSelectionToAnchor: -> @@ -173,8 +174,8 @@ class Movement extends CountPrefix "0": "backward lineboundary" "G": "forward documentboundary" "g": "backward documentboundary" - "w": -> @moveForwardWord() "Y": -> @selectLexicalEntity "lineboundary" + "w": -> @moveForwardWord() "o": -> @reverseSelection() constructor: (options) -> @@ -230,11 +231,14 @@ class Movement extends CountPrefix action() for [0...count] @scrollIntoView() - # Yank the selection; always exits; returns the yanked text. + # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() - @selection.deleteFromDocument() if args.deleteFromDocument or @options.deleteFromDocument console.log "yank:", @yankedText if @debug + if args.deleteFromDocument or @options.deleteFromDocument + @selection.deleteFromDocument() + else + @collapseSelectionToAnchor() message = @yankedText.replace /\s+/g, " " length = @yankedText.length @@ -246,14 +250,18 @@ class Movement extends CountPrefix @exit() @yankedText - # Select a lexical entity, such as a word, a line, or a sentence. The entity should be a Chrome movement - # type, such as "word" or "lineboundary". This assumes that the selection is initially collapsed. - selectLexicalEntity: (entity) -> + # Select a lexical entity, such as a word, or a sentence. The entity should be a Chrome movement type, such + # as "word" or "lineboundary". + selectLexicalEntity: (entity, count = 1) -> + # Locate the start of the current entity. + @runMovement forward, entity + @runMovement backward, entity + @collapseSelectionToFocus() if @options.oneMovementOnly + for [0...count] + @runMovement forward, entity + @runMovement forward, character @runMovement forward, entity - @selection.collapseToEnd() @runMovement backward, entity - # Move the end of the preceding entity. - @runMovements [ backward, entity ], [ forward, entity ] # Try to scroll the focus into view. scrollIntoView: -> @@ -325,20 +333,22 @@ class VisualMode extends Movement # For "yy" and "dd". if @options.yankLineCharacter - @commands[@options.yankLineCharacter] = -> + @commands[@options.yankLineCharacter] = (count) -> if @keypressCount == 1 - @selectLexicalEntity "lineboundary" + @selectLexicalEntity "lineboundary", count @yank() # For "daw", "cas", and so on. if @options.oneMovementOnly - @commands.a = -> + @commands.a = (count) -> if @keypressCount == 1 + # We do not include "paragraph", here. Chrome's paragraph movements seem to be asymmetrical, + # meaning "dap" ends up deleting the wrong text. for entity in [ "word", "sentence", "paragraph" ] do (entity) => - @movements[entity.charAt 0] = -> + @commands[entity.charAt 0] = -> if @keypressCount == 2 - @selectLexicalEntity entity + @selectLexicalEntity entity, count @yank() unless @options.editModeParent @@ -361,8 +371,6 @@ class VisualMode extends Movement super @clipboardContents = text exit: (event, target) -> - @collapseSelectionToAnchor() if @yankedText or @options.editModeParent - unless @options.editModeParent # Don't leave the user in insert mode just because they happen to have selected text within an input # element. @@ -464,15 +472,15 @@ class EditMode extends Movement P: -> @pasteClipboard backward v: -> @launchSubMode VisualMode - Y: -> @enterVisualModeForMovement immediateMovement: "Y" - x: -> @enterVisualModeForMovement immediateMovement: "h", deleteFromDocument: true - X: -> @enterVisualModeForMovement immediateMovement: "l", deleteFromDocument: true - y: -> @enterVisualModeForMovement yankLineCharacter: "y" - d: -> @enterVisualModeForMovement yankLineCharacter: "d", deleteFromDocument: true - c: -> @enterVisualModeForMovement deleteFromDocument: true, onYank: => @enterInsertMode() + Y: (count) -> @enterVisualModeForMovement 1, immediateMovement: "Y" + x: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true + X: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true + y: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "y" + d: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "d", deleteFromDocument: true + c: (count) -> @enterVisualModeForMovement count, deleteFromDocument: true, onYank: => @enterInsertMode() - D: -> @enterVisualModeForMovement immediateMovement: "$", deleteFromDocument: true - C: -> @enterVisualModeForMovement immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() + D: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true + C: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() # Disabled as potentially confusing. # # If the input is empty, then enter insert mode immediately. @@ -481,10 +489,10 @@ class EditMode extends Movement # @enterInsertMode() # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 - enterVisualModeForMovement: (options = {}) -> + enterVisualModeForMovement: (count, options = {}) -> @launchSubMode VisualMode, extend options, badge: "M" - initialCountPrefix: @getCountPrefix() + initialCountPrefix: count oneMovementOnly: true enterInsertMode: () -> @@ -538,23 +546,19 @@ 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 tabs. Or, the user may + # 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. # # How this gets cleaned up is a bit tricky. The suspended-edit mode remains active on the current input - # element indefinately. However, the only way to enter edit mode is via focusInput. And all modes + # 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 a newly-activated 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. # - suspendedEditmode = new Mode - name: "#{@id}-suspended" - singleton: @options.singleton - - suspendedEditmode.push + (new Mode name: "#{@id}-suspended", singleton: @options.singleton).push _name: "suspended-edit/#{@id}/focus" focus: (event) => @alwaysContinueBubbling => -- cgit v1.2.3 From 786b8ba0854b71e7bee00248b3ee29da357ba8d0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 28 Jan 2015 10:32:53 +0000 Subject: Visual/edit modes: yet more minor changes. - Use a has for singletons (as it was previously), but with a distinct identity generated by Utils.getIdentity. - Fix counts not be using in a number of places. --- content_scripts/mode_visual_edit.coffee | 83 +++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 35 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index d7188c1f..5772e28a 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -29,8 +29,7 @@ class CountPrefix extends SuppressPrintable super options @countPrefix = "" - @countPrefixFactor = 1 - @countPrefixFactor = @getCountPrefix options.initialCountPrefix if options.initialCountPrefix + @countPrefixFactor = options.initialCountPrefix || 1 @push _name: "#{@id}/maintain-count" @@ -46,7 +45,6 @@ class CountPrefix extends SuppressPrintable # This handles both "d3w" and "3dw". Also, "3d2w" deletes six words. getCountPrefix: (prefix = @countPrefix) -> - prefix = prefix.toString() if typeof prefix == "number" count = @countPrefixFactor * if 0 < prefix?.length then parseInt prefix else 1 @countPrefix = "" @countPrefixFactor = 1 @@ -131,12 +129,12 @@ class Movement extends CountPrefix forward # An approximation of the vim "w" movement; only ever used in the forward direction. - moveForwardWord: -> + moveForwardWord: (count = 1) -> # First, move to the start of the current word... @runMovement forward, character @runMovement backward, "word" # And then to the start of the next word... - @selectLexicalEntity "word" + @selectLexicalEntity "word", count return # Previous version... # This works in normal text inputs, but not in some contentEditable elements (notably the compose window @@ -174,9 +172,9 @@ class Movement extends CountPrefix "0": "backward lineboundary" "G": "forward documentboundary" "g": "backward documentboundary" - "Y": -> @selectLexicalEntity "lineboundary" - "w": -> @moveForwardWord() - "o": -> @reverseSelection() + "Y": (count) -> @selectLexicalEntity "lineboundary", count + "w": (count) -> @moveForwardWord count + "o": (count) -> @reverseSelection() constructor: (options) -> @selection = window.getSelection() @@ -192,8 +190,8 @@ class Movement extends CountPrefix @movements.W = @movements.w if @options.immediateMovement - # This instance has been created just to run a single movement only and then yank the result. - @handleMovementKeyChar @options.immediateMovement + # This instance has been created just to run a single movement then yank the result. + @handleMovementKeyChar @options.immediateMovement, @getCountPrefix() @yank() return @@ -221,14 +219,17 @@ class Movement extends CountPrefix return @suppressEvent @continueBubbling + # + # End of Movement constructor. handleMovementKeyChar: (keyChar, count = 1) -> - action = - switch typeof @movements[keyChar] - when "string" then => @runMovement @movements[keyChar] - when "function" then => @movements[keyChar].call @ + console.log "xxx", keyChar, count @protectClipboard => - action() for [0...count] + 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. @@ -306,7 +307,7 @@ class VisualMode extends Movement return when "Caret" # Try to start with a visible selection. - @extendByOneCharacter(forward) or @extendByOneCharacter backward unless options.editModeParent + @extendByOneCharacter(forward) or @extendByOneCharacter backward unless options.parentMode @scrollIntoView() if @selection.type == "Range" defaults = @@ -319,17 +320,21 @@ class VisualMode extends Movement # Additional commands when not being run only for movement. unless @options.oneMovementOnly @commands.y = -> @yank() - @commands.V = -> new VisualLineMode @commands.p = -> chrome.runtime.sendMessage handler: "openUrlInCurrentTab", url: @yank() @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() - - # Additional commands when run under edit mode (but not just for movement). - if @options.editModeParent and not @options.oneMovementOnly + @commands.V = -> + if @options.parentMode + @options.parentMode.launchSubMode VisualLineMode + else + new VisualLineMode + + # Additional commands when run under edit mode (except if only for one movement). + if @options.parentMode and not @options.oneMovementOnly @commands.x = -> @yank deleteFromDocument: true @commands.d = -> @yank deleteFromDocument: true @commands.c = -> @yank deleteFromDocument: true - @options.editModeParent.enterInsertMode() + @options.parentMode.enterInsertMode() # For "yy" and "dd". if @options.yankLineCharacter @@ -351,7 +356,7 @@ class VisualMode extends Movement @selectLexicalEntity entity, count @yank() - unless @options.editModeParent + unless @options.parentMode @installFindMode() # Grab the initial clipboard contents. We try to keep them intact until we get an explicit yank. @@ -371,7 +376,7 @@ class VisualMode extends Movement super @clipboardContents = text exit: (event, target) -> - unless @options.editModeParent + unless @options.parentMode # Don't leave the user in insert mode just because they happen to have selected text within an input # element. if document.activeElement and DomUtils.isEditable document.activeElement @@ -412,8 +417,8 @@ class VisualMode extends Movement range.setStart newFindRange.startContainer, newFindRange.startOffset @selectRange range - @movements.n = -> executeFind false - @movements.N = -> executeFind true + @movements.n = (count) -> executeFind false + @movements.N = (count) -> executeFind true # When visual mode starts and there's no existing selection, we try to establish one. As a heuristic, we # pick the first non-whitespace character of the first visible text node which seems to be long enough to be @@ -439,6 +444,12 @@ class VisualLineMode extends VisualMode super extend { name: "visual/line" }, options @extendSelection() + @commands.v = -> + if @options.parentMode + @options.parentMode.launchSubMode VisualMode + else + new VisualMode + handleMovementKeyChar: (keyChar) -> super keyChar @extendSelection() @@ -471,9 +482,10 @@ class EditMode extends Movement p: -> @pasteClipboard forward P: -> @pasteClipboard backward v: -> @launchSubMode VisualMode + V: -> @launchSubMode VisualLineMode - Y: (count) -> @enterVisualModeForMovement 1, immediateMovement: "Y" - x: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true + Y: (count) -> @enterVisualModeForMovement count, immediateMovement: "Y" + x: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true X: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true y: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "y" d: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "d", deleteFromDocument: true @@ -501,9 +513,11 @@ class EditMode extends Movement targetElement: @options.targetElement launchSubMode: (mode, options = {}) -> - @lastSubMode = + @activeSubMode?.instance.exit() + @activeSubMode = mode: mode - instance: new mode extend options, editModeParent: @ + instance: new mode extend options, parentMode: @ + @activeSubMode.instance.onExit => @activeSubMode = null pasteClipboard: (direction) -> @paste (text) => @@ -535,10 +549,9 @@ class EditMode extends Movement exit: (event, target) -> super event, target - @lastSubMode = - if @lastSubMode?.instance.modeIsActive - @lastSubMode.instance.exit event, target - @lastSubMode + # Deactivate any active sub-mode. Any such mode will clear @activeSubMode on exit, so we grab a copy now. + activeSubMode = @activeSubMode + activeSubMode?.instance.exit() if event?.type == "keydown" and KeyboardUtils.isEscape event if target? and DomUtils.isDOMDescendant @element, target @@ -565,8 +578,8 @@ class EditMode extends Movement if event?.target == @options.targetElement console.log "#{@id}: reactivating edit mode" if @debug editMode = new EditMode @getConfigurationOptions() - if @lastSubMode - editMode.launchSubMode @lastSubMode.mode, @lastSubMode.instance.getConfigurationOptions() + if activeSubMode + editMode.launchSubMode activeSubMode.mode, activeSubMode.instance.getConfigurationOptions() root = exports ? window root.VisualMode = VisualMode -- cgit v1.2.3 From d6450ed2122c3bf539c9ea837d4423a69f043a68 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 28 Jan 2015 14:00:38 +0000 Subject: Visual/edit modes: fix some movements... Also, change how vim's "w" is implemented. Also some code-review/cleanup. Better dd and yy for gmail. --- content_scripts/mode_visual_edit.coffee | 251 +++++++++++++++++++------------- 1 file changed, 148 insertions(+), 103 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 5772e28a..bd1b0616 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -2,21 +2,29 @@ # Todo: # Konami code? # Use find as a mode. -# Perhaps refactor visual/movement modes. # Exit on Ctrl-Enter. # Scroll is broken (again). Seems to be after dd. +# Paste of whole lines. +# Arrow keys. +# J # This prevents printable characters from being passed through to the underlying page. It should, however, -# allow through Chrome keyboard shortcuts. It's a keyboard-event backstop for visual mode and edit mode. +# allow through Chrome keyboard shortcuts. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => - return @stopBubblingAndTrue unless KeyboardUtils.isPrintable event - return @suppressEvent unless event.type == "keydown" - # Completely suppress Backspace and Delete. - return @suppressEvent if event.keyCode in [ 8, 46 ] - DomUtils.suppressPropagation event - @stopBubblingAndFalse + if KeyboardUtils.isPrintable event + if event.type == "keydown" + # Completely suppress Backspace and Delete, they change the selection. + if event.keyCode in [ 8, 46 ] + @suppressEvent + else + DomUtils.suppressPropagation + @stopBubblingAndFalse + else + @suppressEvent + else + @stopBubblingAndTrue super extend options, keydown: handler @@ -26,34 +34,34 @@ class SuppressPrintable extends Mode # This watches keypresses and maintains the count prefix as number keys and other keys are pressed. class CountPrefix extends SuppressPrintable constructor: (options) -> - super options - @countPrefix = "" @countPrefixFactor = options.initialCountPrefix || 1 + super options @push - _name: "#{@id}/maintain-count" + _name: "#{@id}/count-prefix" keypress: (event) => @alwaysContinueBubbling => unless event.metaKey or event.ctrlKey or event.altKey keyChar = String.fromCharCode event.charCode @countPrefix = - if keyChar?.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0" + if keyChar.length == 1 and "0" <= keyChar <= "9" and @countPrefix + keyChar != "0" @countPrefix + keyChar else "" # This handles both "d3w" and "3dw". Also, "3d2w" deletes six words. - getCountPrefix: (prefix = @countPrefix) -> - count = @countPrefixFactor * if 0 < prefix?.length then parseInt prefix else 1 + getCountPrefix: -> + count = @countPrefixFactor * if 0 < @countPrefix?.length then parseInt @countPrefix else 1 @countPrefix = "" @countPrefixFactor = 1 count -# Some symbolic names for frequently-used strings. +# Some symbolic names for common strings. forward = "forward" backward = "backward" character = "character" +vimword = "vimword" # This implements movement commands with count prefixes for both visual mode and edit mode. class Movement extends CountPrefix @@ -65,25 +73,53 @@ class Movement extends CountPrefix paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response - # Return a value which changes whenever the selection changes. - hashSelection: -> - [ @element?.selectionStart, @selection.toString().length ].join "/" - - # Call a function; return true if the selection changed. - selectionChanged: (func) -> - before = @hashSelection(); func(); @hashSelection() != before + # Return the character following the focus, and leave the selection unchanged. + nextCharacter: -> + beforeText = @selection.toString() + 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] + else + beforeText[0] - # Run a movement. For convenience, the following three forms can be used: + # Run a movement. For convenience, the following three argument forms are available: # @runMovement "forward word" # @runMovement [ "forward", "word" ] # @runMovement "forward", "word" runMovement: (args...) -> + # Normalize the various argument forms (to an array of two strings: direction and granularity). movement = if typeof(args[0]) == "string" and args.length == 1 args[0].trim().split /\s+/ else if args.length == 1 then args[0] else args[...2] - @selection.modify @alterMethod, movement... + + console.log "movement:", movement + + # Perform the movement. + # We use the pseudo-granularity "vimword" to implement vim's "w" movement (which is not supported + # natively). + if movement[1] == "vimword" and movement[0] == forward + if /\s/.test @nextCharacter() + @runMovements [ forward, "word" ], [ backward, "word" ] + else + @runMovements [ forward, "word" ], [ forward, vimword ] + + else if movement[1] == "vimword" + @selection.modify @alterMethod, backward, "word" + + else + @selection.modify @alterMethod, movement... + + # Return a simple camparable value which will be different for different selections. + hashSelection: -> + [ @element?.selectionStart, @selection.toString().length ].join "/" + + # Call a function; return true if the selection changed. + selectionChanged: (func) -> + before = @hashSelection(); func(); @hashSelection() != before # Run a sequence of movements, stopping if a movement fails to change the selection. runMovements: (movements...) -> @@ -91,13 +127,13 @@ class Movement extends CountPrefix return false unless @selectionChanged => @runMovement movement true - # Swap the anchor node/offset and the focus node/offset. + # Swap the anchor node/offset and the focus node/offset (which 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 inefficient if the selection is large. We only use - # it if we have to. However, the normal method (below) does not work for simple text inputs. + # 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. length = @selection.toString().length @collapseSelectionToFocus() @runMovement @opposite[direction], character for [0...length] @@ -128,23 +164,6 @@ class Movement extends CountPrefix return if 0 < change then direction else @opposite[direction] forward - # An approximation of the vim "w" movement; only ever used in the forward direction. - moveForwardWord: (count = 1) -> - # First, move to the start of the current word... - @runMovement forward, character - @runMovement backward, "word" - # And then to the start of the next word... - @selectLexicalEntity "word", count - return - # Previous version... - # This works in normal text inputs, but not in some contentEditable elements (notably the compose window - # on Google's Inbox). - # First, move to the end of the preceding word... - if @runMovements "forward character", "backward word", "forward word" - # And then to the start of the following word... - # The two character movements allow us to also get to the end of the very-last word. - @runMovements "forward word", "forward character", "backward character", "backward word" - collapseSelectionToAnchor: -> if 0 < @selection.toString().length @selection[if @getDirection() == backward then "collapseToEnd" else "collapseToStart"]() @@ -157,6 +176,8 @@ class Movement extends CountPrefix @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. movements: "l": "forward character" "h": "backward character" @@ -164,6 +185,7 @@ class Movement extends CountPrefix "k": "backward line" "e": "forward word" "b": "backward word" + "w": "forward vimword" ")": "forward sentence" "(": "backward sentence" "}": "forward paragraph" @@ -171,10 +193,9 @@ class Movement extends CountPrefix "$": "forward lineboundary" "0": "backward lineboundary" "G": "forward documentboundary" - "g": "backward documentboundary" - "Y": (count) -> @selectLexicalEntity "lineboundary", count - "w": (count) -> @moveForwardWord count - "o": (count) -> @reverseSelection() + "gg": "backward documentboundary" + "Y": (count) -> @selectLine count, false + "o": -> @reverseSelection() constructor: (options) -> @selection = window.getSelection() @@ -190,7 +211,7 @@ class Movement extends CountPrefix @movements.W = @movements.w if @options.immediateMovement - # This instance has been created just to run a single movement then yank the result. + # Run a single movement then yank the result (note, yank() exits). @handleMovementKeyChar @options.immediateMovement, @getCountPrefix() @yank() return @@ -223,7 +244,6 @@ class Movement extends CountPrefix # End of Movement constructor. handleMovementKeyChar: (keyChar, count = 1) -> - console.log "xxx", keyChar, count @protectClipboard => switch typeof @movements[keyChar] when "string" @@ -236,33 +256,36 @@ class Movement extends CountPrefix yank: (args = {}) -> @yankedText = @selection.toString() console.log "yank:", @yankedText if @debug + if args.deleteFromDocument or @options.deleteFromDocument @selection.deleteFromDocument() else @collapseSelectionToAnchor() message = @yankedText.replace /\s+/g, " " - length = @yankedText.length - message = message[...12] + "..." if 15 < length - plural = if length == 1 then "" else "s" - HUD.showForDuration "Yanked #{length} character#{plural}: \"#{message}\".", 2500 + message = message[...12] + "..." if 15 < @yankedText.length + plural = if @yankedText.length == 1 then "" else "s" + HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 @options.onYank.call @, @yankedText if @options.onYank @exit() @yankedText - # Select a lexical entity, such as a word, or a sentence. The entity should be a Chrome movement type, such + # Select a lexical entity, such as a word, or a sentence. The entity should be a movement granularity such # as "word" or "lineboundary". selectLexicalEntity: (entity, count = 1) -> # Locate the start of the current entity. @runMovement forward, entity @runMovement backward, entity @collapseSelectionToFocus() if @options.oneMovementOnly + # Move over count entities. for [0...count] - @runMovement forward, entity + 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 - @runMovement forward, entity - @runMovement backward, entity + # Move to the start of the subsequent entity + @runMovements [ forward, entity ], [ backward, entity ] # Try to scroll the focus into view. scrollIntoView: -> @@ -276,25 +299,9 @@ class Movement extends CountPrefix coords = DomUtils.getCaretCoordinates @element, position Scroller.scrollToPosition @element, coords.top, coords.left else - elementWithFocus = @getElementWithFocus @selection + elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward 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. - getElementWithFocus: (selection) -> - r = t = selection.getRangeAt 0 - if selection.type == "Range" - r = t.cloneRange() - r.collapse @getDirection() == backward - t = r.startContainer - t = t.childNodes[r.startOffset] if t.nodeType == 1 - o = t - o = o.previousSibling while o and o.nodeType != 1 - t = o || t?.parentNode - t - class VisualMode extends Movement constructor: (options = {}) -> @selection = window.getSelection() @@ -306,7 +313,7 @@ class VisualMode extends Movement HUD.showForDuration "Create a selection before entering visual mode.", 2500 return when "Caret" - # Try to start with a visible selection. + # Try to make the selection visible (unless we're under a parent mode, such as edit mode). @extendByOneCharacter(forward) or @extendByOneCharacter backward unless options.parentMode @scrollIntoView() if @selection.type == "Range" @@ -317,12 +324,13 @@ class VisualMode extends Movement exitOnEscape: true super extend defaults, options - # Additional commands when not being run only for movement. + # Visual-mode commands. unless @options.oneMovementOnly @commands.y = -> @yank() @commands.p = -> chrome.runtime.sendMessage handler: "openUrlInCurrentTab", url: @yank() @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() @commands.V = -> + @exit() if @options.parentMode @options.parentMode.launchSubMode VisualLineMode else @@ -336,19 +344,17 @@ class VisualMode extends Movement @yank deleteFromDocument: true @options.parentMode.enterInsertMode() - # For "yy" and "dd". + # For edit mode's "yy" and "dd". if @options.yankLineCharacter @commands[@options.yankLineCharacter] = (count) -> if @keypressCount == 1 - @selectLexicalEntity "lineboundary", count + @selectLine count, true @yank() - # For "daw", "cas", and so on. + # For edit mode's "daw", "cas", and so on. if @options.oneMovementOnly @commands.a = (count) -> if @keypressCount == 1 - # We do not include "paragraph", here. Chrome's paragraph movements seem to be asymmetrical, - # meaning "dap" ends up deleting the wrong text. for entity in [ "word", "sentence", "paragraph" ] do (entity) => @commands[entity.charAt 0] = -> @@ -366,15 +372,6 @@ class VisualMode extends Movement # # End of VisualMode constructor. - # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We - # always reinstall the original clipboard contents when we're done. - protectClipboard: (func) -> - func() - @copy @clipboardContents if @clipboardContents - - copy: (text) -> - super @clipboardContents = text - exit: (event, target) -> unless @options.parentMode # Don't leave the user in insert mode just because they happen to have selected text within an input @@ -385,6 +382,20 @@ class VisualMode extends Movement super event, target @copy @yankedText if @yankedText + selectLine: (count, collapse) -> + @runMovement backward, "lineboundary" + @collapseSelectionToFocus() if collapse + @runMovement forward, "line" for [0...count] + + # This is used whenever manipulating the selection may, as a side effect, change the clipboard contents. It + # reinstalls the original clipboard contents when we're done. + protectClipboard: (func) -> + func() + @copy @clipboardContents if @clipboardContents + + copy: (text) -> + super @clipboardContents = text + # FIXME(smblott). This is a mess, it needs to be reworked. Ideally, incorporate FindMode. installFindMode: -> previousFindRange = null @@ -397,14 +408,14 @@ class VisualMode extends Movement initialRange = @selection.getRangeAt(0).cloneRange() direction = @getDirection() - # Re-selecting the previous match, if any; this tells Chrome where to start. + # Re-select the previous match, if any; this tells Chrome where to start. @selectRange previousFindRange if previousFindRange window.find query, caseSensitive, findBackwards, true, false, true, false previousFindRange = newFindRange = @selection.getRangeAt(0).cloneRange() # FIXME(smblott). What if there are no matches? - # Install a new range from the original selection anchor to end of the new match. + # Install a new range from the original selection anchor to the end of the new match. range = document.createRange() which = if direction == forward then "start" else "end" range.setStart initialRange["#{which}Container"], initialRange["#{which}Offset"] @@ -445,13 +456,14 @@ class VisualLineMode extends VisualMode @extendSelection() @commands.v = -> + @exit() if @options.parentMode @options.parentMode.launchSubMode VisualMode else new VisualMode - handleMovementKeyChar: (keyChar) -> - super keyChar + handleMovementKeyChar: (args...) -> + super args... @extendSelection() extendSelection: -> @@ -476,6 +488,7 @@ class EditMode extends Movement extend @commands, i: -> @enterInsertMode() a: -> @enterInsertMode() + I: -> @runMovement "backward lineboundary"; @enterInsertMode() A: -> @runMovement "forward lineboundary"; @enterInsertMode() o: -> @openLine forward O: -> @openLine backward @@ -486,7 +499,7 @@ class EditMode extends Movement Y: (count) -> @enterVisualModeForMovement count, immediateMovement: "Y" x: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true - X: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true + X: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true y: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "y" d: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "d", deleteFromDocument: true c: (count) -> @enterVisualModeForMovement count, deleteFromDocument: true, onYank: => @enterInsertMode() @@ -494,6 +507,26 @@ class EditMode extends Movement D: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true C: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() + J: (count) -> + for [0...count] + @runMovement forward, "lineboundary" + @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true + DomUtils.simulateTextEntry @element, " " + + r: (count) -> + handlerStack.push + _name: "repeat-character" + keydown: (event) => + handlerStack.remove() + if KeyboardUtils.isPrintable event + keyChar = KeyboardUtils.getKeyChar(event).toLowerCase() + @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true + DomUtils.simulateTextEntry @element, [0...count].map(-> keyChar).join "" + @suppressEvent + + '~': (count) -> @swapCase count, true + 'g~': (count) -> @swapCase count, false + # Disabled as potentially confusing. # # If the input is empty, then enter insert mode immediately. # unless @element.isContentEditable @@ -501,6 +534,19 @@ class EditMode extends Movement # @enterInsertMode() # HUD.showForDuration "Input empty, entered insert mode directly.", 3500 + swapCase: (count, immediate) -> + @enterVisualModeForMovement count, + immediateMovement: if immediate then "l" else null + deleteFromDocument: true + onYank: (text) => + chars = + for char in text.split "" + if char == char.toLowerCase() + char.toUpperCase() + else + char.toLowerCase() + DomUtils.simulateTextEntry @element, chars.join "" + enterVisualModeForMovement: (count, options = {}) -> @launchSubMode VisualMode, extend options, badge: "M" @@ -525,13 +571,13 @@ class EditMode extends Movement openLine: (direction) -> @runMovement direction, "lineboundary" - @enterInsertMode() DomUtils.simulateTextEntry @element, "\n" @runMovement backward, character if direction == backward + @enterInsertMode() # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We - # always reinstall the original clipboard contents when we're done. Note, this may be asynchronous. We do - # this this way (as opposed to the simpler, synchronous method used by Visual mode) because the user may + # always restore the original clipboard contents when we're done. Note, this may be asynchronous. We use + # this approach (as opposed to the simpler, synchronous method used by Visual mode) because the user may # wish to select text with the mouse (while edit mode is active) to later paste with "p" or "P". protectClipboard: do -> locked = false @@ -567,16 +613,15 @@ class EditMode extends Movement # 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 a newly-activated 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. + # 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 _name: "suspended-edit/#{@id}/focus" focus: (event) => @alwaysContinueBubbling => if event?.target == @options.targetElement - console.log "#{@id}: reactivating edit mode" if @debug editMode = new EditMode @getConfigurationOptions() if activeSubMode editMode.launchSubMode activeSubMode.mode, activeSubMode.instance.getConfigurationOptions() -- cgit v1.2.3 From f2343ebf99cda983b038d485ee1cb01a8d5f265b Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 29 Jan 2015 06:20:01 +0000 Subject: Visual/edit modes: fix w movement for contentEditable. It turns out we need to check different properties of the selection in order to correctly detect when the selection has changed in contentEditable elements. --- content_scripts/mode_visual_edit.coffee | 48 ++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 16 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index bd1b0616..c00378f4 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -96,12 +96,11 @@ class Movement extends CountPrefix else if args.length == 1 then args[0] else args[...2] - console.log "movement:", movement - # Perform the movement. # We use the pseudo-granularity "vimword" to implement vim's "w" movement (which is not supported # natively). if movement[1] == "vimword" and movement[0] == forward + x = @nextCharacter() if /\s/.test @nextCharacter() @runMovements [ forward, "word" ], [ backward, "word" ] else @@ -113,9 +112,21 @@ class Movement extends CountPrefix else @selection.modify @alterMethod, movement... - # Return a simple camparable value which will be different for different selections. - hashSelection: -> - [ @element?.selectionStart, @selection.toString().length ].join "/" + # # Return a simple camparable value which will be different for different selections. + # hashSelection: -> + # [ @element?.selectionStart, @selection.toString().length ].join "/" + + # 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. + 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. selectionChanged: (func) -> @@ -255,7 +266,7 @@ class Movement extends CountPrefix # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() - console.log "yank:", @yankedText if @debug + console.log "yank:", @yankedText if @debug This is used to detect, after a movement, whether the selection has changed. if args.deleteFromDocument or @options.deleteFromDocument @selection.deleteFromDocument() @@ -567,7 +578,16 @@ class EditMode extends Movement pasteClipboard: (direction) -> @paste (text) => - DomUtils.simulateTextEntry @element, text if 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" @@ -576,21 +596,17 @@ class EditMode extends Movement @enterInsertMode() # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We - # always restore the original clipboard contents when we're done. Note, this may be asynchronous. We use - # this approach (as opposed to the simpler, synchronous method used by Visual mode) because the user may - # wish to select text with the mouse (while edit mode is active) to later paste with "p" or "P". + # restore the original clipboard contents when we're done. Note, this may be asynchronous. We use this + # approach (as opposed to the simpler, synchronous method used by Visual mode) because the user may wish to + # select text with the mouse (while edit mode is active), and then paste with "p" or "P". protectClipboard: do -> locked = false (func) -> - if locked - func() + if locked then func() else locked = true - @paste (text) => - func() - @copy text - locked = false + @paste (text) => func(); @copy text; locked = false exit: (event, target) -> super event, target -- cgit v1.2.3 From 6edfb1f7b830652ca715fb16b999afc2efc184a9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 29 Jan 2015 06:31:59 +0000 Subject: Visual/edit modes: fix broken due to stray uncommented comment. --- content_scripts/mode_visual_edit.coffee | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index c00378f4..6da17ff1 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -112,10 +112,6 @@ class Movement extends CountPrefix else @selection.modify @alterMethod, movement... - # # Return a simple camparable value which will be different for different selections. - # hashSelection: -> - # [ @element?.selectionStart, @selection.toString().length ].join "/" - # 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. hashSelection: (debug) -> @@ -266,7 +262,7 @@ class Movement extends CountPrefix # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() - console.log "yank:", @yankedText if @debug This is used to detect, after a movement, whether the selection has changed. + console.log "yank:", @yankedText if @debug if args.deleteFromDocument or @options.deleteFromDocument @selection.deleteFromDocument() -- cgit v1.2.3 From 15f462f2504b3f5858a83b36ffb38bc877cc0b69 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 29 Jan 2015 10:43:44 +0000 Subject: Visual/edit modes: fix key suppression. --- content_scripts/mode_visual_edit.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 6da17ff1..4f0b97c6 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -19,7 +19,7 @@ class SuppressPrintable extends Mode if event.keyCode in [ 8, 46 ] @suppressEvent else - DomUtils.suppressPropagation + DomUtils.suppressPropagation event @stopBubblingAndFalse else @suppressEvent -- cgit v1.2.3 From 21bb741f4045873bda439dc585ac356fc40c595a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 29 Jan 2015 13:04:43 +0000 Subject: Visual/edit modes: more changes... - Simplify the SuppressPrintable key handler. - Rework the protectClipboard logic. - Some commands (x, X, etc) never affect the clipboard. --- content_scripts/mode_visual_edit.coffee | 96 ++++++++++++++------------------- 1 file changed, 41 insertions(+), 55 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 4f0b97c6..f6f6a318 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -13,18 +13,13 @@ class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => - if KeyboardUtils.isPrintable event - if event.type == "keydown" - # Completely suppress Backspace and Delete, they change the selection. - if event.keyCode in [ 8, 46 ] - @suppressEvent - else - DomUtils.suppressPropagation event - @stopBubblingAndFalse - else - @suppressEvent - else - @stopBubblingAndTrue + return @stopBubblingAndTrue if not KeyboardUtils.isPrintable event + return @suppressEvent if event.type != "keydown" + # Completely suppress Backspace and Delete, they change the selection. + @suppressEvent if event.keyCode in [ 8, 46 ] + # Suppress propagation (but not preventDefault) for keydown, printable events. + DomUtils.suppressPropagation event + @stopBubblingAndFalse super extend options, keydown: handler @@ -67,12 +62,29 @@ vimword = "vimword" class Movement extends CountPrefix opposite: forward: backward, backward: forward - copy: (text) -> - chrome.runtime.sendMessage handler: "copyToClipboard", data: text if text - paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response + 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. + @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. + protectClipboard: do -> + locked = false + + (func) -> + if locked then func() + else + locked = true + @paste (text) => + func(); @copy text; locked = false + # Return the character following the focus, and leave the selection unchanged. nextCharacter: -> beforeText = @selection.toString() @@ -210,7 +222,6 @@ class Movement extends CountPrefix @commands = {} @keyQueue = "" @keypressCount = 0 - @yankedText = "" super options # Aliases. @@ -218,9 +229,7 @@ class Movement extends CountPrefix @movements.W = @movements.w if @options.immediateMovement - # Run a single movement then yank the result (note, yank() exits). @handleMovementKeyChar @options.immediateMovement, @getCountPrefix() - @yank() return @push @@ -243,7 +252,6 @@ class Movement extends CountPrefix else if @movements[command] @handleMovementKeyChar command, @getCountPrefix() - @yank() if @options.oneMovementOnly return @suppressEvent @continueBubbling @@ -258,11 +266,11 @@ class Movement extends CountPrefix when "function" @movements[keyChar].call @, count @scrollIntoView() + @yank() if @options.oneMovementOnly # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() - console.log "yank:", @yankedText if @debug if args.deleteFromDocument or @options.deleteFromDocument @selection.deleteFromDocument() @@ -371,11 +379,6 @@ class VisualMode extends Movement unless @options.parentMode @installFindMode() - - # Grab the initial clipboard contents. We try to keep them intact until we get an explicit yank. - @clipboardContents = "" - @paste (text) => - @clipboardContents = text if text # # End of VisualMode constructor. @@ -387,22 +390,16 @@ class VisualMode extends Movement document.activeElement.blur() super event, target - @copy @yankedText if @yankedText + if @yankedText? and not @options.noCopyToClipboard + console.log "yank:", @yankedText if @debug + @copy @yankedText, true selectLine: (count, collapse) -> @runMovement backward, "lineboundary" @collapseSelectionToFocus() if collapse @runMovement forward, "line" for [0...count] - # This is used whenever manipulating the selection may, as a side effect, change the clipboard contents. It - # reinstalls the original clipboard contents when we're done. - protectClipboard: (func) -> - func() - @copy @clipboardContents if @clipboardContents - - copy: (text) -> - super @clipboardContents = text - + # This installs a basic binding for find mode, "n" and "N". # FIXME(smblott). This is a mess, it needs to be reworked. Ideally, incorporate FindMode. installFindMode: -> previousFindRange = null @@ -505,8 +502,8 @@ class EditMode extends Movement V: -> @launchSubMode VisualLineMode Y: (count) -> @enterVisualModeForMovement count, immediateMovement: "Y" - x: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true - X: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true + x: (count) -> @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true, noCopyToClipboard: true + X: (count) -> @enterVisualModeForMovement count, immediateMovement: "h", deleteFromDocument: true, noCopyToClipboard: true y: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "y" d: (count) -> @enterVisualModeForMovement count, yankLineCharacter: "d", deleteFromDocument: true c: (count) -> @enterVisualModeForMovement count, deleteFromDocument: true, onYank: => @enterInsertMode() @@ -517,17 +514,18 @@ class EditMode extends Movement J: (count) -> for [0...count] @runMovement forward, "lineboundary" - @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true + @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true, noCopyToClipboard: true DomUtils.simulateTextEntry @element, " " r: (count) -> handlerStack.push _name: "repeat-character" - keydown: (event) => + keydown: (event) => DomUtils.suppressPropagation event; @stopBubblingAndFalse + keypress: (event) => handlerStack.remove() - if KeyboardUtils.isPrintable event - keyChar = KeyboardUtils.getKeyChar(event).toLowerCase() - @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true + keyChar = String.fromCharCode event.charCode + if keyChar.length == 1 + @enterVisualModeForMovement count, immediateMovement: "l", deleteFromDocument: true, noCopyToClipboard: true DomUtils.simulateTextEntry @element, [0...count].map(-> keyChar).join "" @suppressEvent @@ -545,6 +543,7 @@ class EditMode extends Movement @enterVisualModeForMovement count, immediateMovement: if immediate then "l" else null deleteFromDocument: true + noCopyToClipboard: true onYank: (text) => chars = for char in text.split "" @@ -591,19 +590,6 @@ class EditMode extends Movement @runMovement backward, character if direction == backward @enterInsertMode() - # This used whenever manipulating the selection may, as a side effect, change the clipboard contents. We - # restore the original clipboard contents when we're done. Note, this may be asynchronous. We use this - # approach (as opposed to the simpler, synchronous method used by Visual mode) because the user may wish to - # select text with the mouse (while edit mode is active), and then paste with "p" or "P". - protectClipboard: do -> - locked = false - - (func) -> - if locked then func() - else - locked = true - @paste (text) => func(); @copy text; locked = false - exit: (event, target) -> super event, target -- cgit v1.2.3 From f3128787644670350be18a505c32eedb1c26a86a Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 29 Jan 2015 17:51:28 +0000 Subject: Visual/edit modes: initial caret mode. --- content_scripts/mode_visual_edit.coffee | 144 ++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 52 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index f6f6a318..d3d51575 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -85,6 +85,13 @@ class Movement extends CountPrefix @paste (text) => func(); @copy text; locked = false + changeMode: (mode, options = {}) -> + @exit() + if @options.parentMode + @options.parentMode.launchSubMode mode, options + else + new mode + # Return the character following the focus, and leave the selection unchanged. nextCharacter: -> beforeText = @selection.toString() @@ -229,7 +236,7 @@ class Movement extends CountPrefix @movements.W = @movements.w if @options.immediateMovement - @handleMovementKeyChar @options.immediateMovement, @getCountPrefix() + @runMovementKeyChar @options.immediateMovement, @getCountPrefix() return @push @@ -251,22 +258,24 @@ class Movement extends CountPrefix return @suppressEvent else if @movements[command] - @handleMovementKeyChar command, @getCountPrefix() + @runMovementKeyChar command, @getCountPrefix() return @suppressEvent @continueBubbling + # # End of Movement constructor. + runMovementKeyChar: (args...) -> + @protectClipboard => @handleMovementKeyChar args... + handleMovementKeyChar: (keyChar, count = 1) -> - @protectClipboard => - switch typeof @movements[keyChar] - when "string" - @runMovement @movements[keyChar] for [0...count] - when "function" - @movements[keyChar].call @, count - @scrollIntoView() - @yank() if @options.oneMovementOnly + 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 = {}) -> @@ -322,16 +331,6 @@ class VisualMode extends Movement @selection = window.getSelection() @alterMethod = "extend" - switch @selection.type - when "None" - unless @establishInitialSelection() - HUD.showForDuration "Create a selection before entering visual mode.", 2500 - return - when "Caret" - # Try to make the selection visible (unless we're under a parent mode, such as edit mode). - @extendByOneCharacter(forward) or @extendByOneCharacter backward unless options.parentMode - @scrollIntoView() if @selection.type == "Range" - defaults = name: "visual" badge: "V" @@ -339,17 +338,27 @@ class VisualMode extends Movement exitOnEscape: true super extend defaults, options + switch @selection.type + when "None" + return @changeMode CaretMode + when "Caret" + @selection.modify "extend", forward, character + + # Yank on . + @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 + # Visual-mode commands. unless @options.oneMovementOnly @commands.y = -> @yank() @commands.p = -> chrome.runtime.sendMessage handler: "openUrlInCurrentTab", url: @yank() @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() - @commands.V = -> - @exit() - if @options.parentMode - @options.parentMode.launchSubMode VisualLineMode - else - new VisualLineMode + @commands.V = -> @changeMode VisualLineMode + @commands.c = -> @changeMode CaretMode # Additional commands when run under edit mode (except if only for one movement). if @options.parentMode and not @options.oneMovementOnly @@ -394,6 +403,10 @@ class VisualMode extends Movement console.log "yank:", @yankedText if @debug @copy @yankedText, true + handleMovementKeyChar: (args...) -> + super args... + @yank() if @options.oneMovementOnly + selectLine: (count, collapse) -> @runMovement backward, "lineboundary" @collapseSelectionToFocus() if collapse @@ -435,36 +448,11 @@ class VisualMode extends Movement @movements.n = (count) -> executeFind false @movements.N = (count) -> executeFind true - # When visual mode starts and there's no existing selection, we try to establish one. 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. - establishInitialSelection: -> - nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT - while node = nodes.nextNode() - # Don't pick really short texts; they're likely to be part of a banner. - if node.nodeType == 3 and 50 <= node.data.trim().length - element = node.parentElement - if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element - offset = node.data.length - node.data.replace(/^\s+/, "").length - range = document.createRange() - range.setStart node, offset - range.setEnd node, offset + 1 - @selectRange range - @scrollIntoView() - return true - false - class VisualLineMode extends VisualMode constructor: (options = {}) -> super extend { name: "visual/line" }, options @extendSelection() - - @commands.v = -> - @exit() - if @options.parentMode - @options.parentMode.launchSubMode VisualMode - else - new VisualMode + @commands.v = -> @changeMode VisualMode handleMovementKeyChar: (args...) -> super args... @@ -476,6 +464,58 @@ class VisualLineMode extends VisualMode @runMovement direction, "lineboundary" @reverseSelection() +class CaretMode extends Movement + constructor: (options = {}) -> + @alterMethod = "move" + + defaults = + name: "caret" + badge: "C" + singleton: VisualMode + exitOnEscape: true + super extend defaults, options + + if @selection.type == "None" + @establishInitialSelection() + + switch @selection.type + when "None" + HUD.showForDuration "Create a selection before entering visual mode.", 2500 + @exit() + return + when "Range" + @collapseSelectionToFocus() + + @selection.modify "extend", forward, character + @scrollIntoView() + + extend @commands, + v: -> @changeMode VisualMode + V: -> @changeMode VisualLineMode + + handleMovementKeyChar: (args...) -> + @collapseSelectionToAnchor() + super args... + @selection.modify "extend", forward, character + + # 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. + establishInitialSelection: -> + nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT + while node = nodes.nextNode() + # Don't pick really short texts; they're likely to be part of a banner. + if node.nodeType == 3 and 50 <= node.data.trim().length + element = node.parentElement + if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element + offset = node.data.length - node.data.replace(/^\s+/, "").length + range = document.createRange() + range.setStart node, offset + range.setEnd node, offset+1 + @selectRange range + return true + false + class EditMode extends Movement constructor: (options = {}) -> @element = document.activeElement -- cgit v1.2.3 From 4fa7870c15ed4abf9196aac9d81c1672c96d0d65 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 06:06:45 +0000 Subject: Visual/edit modes: miscellaneous improvements.... - better definition of vimword. - fix initial selection as just one character when entering caret mode. - fix initial caret when entering visual mode from edit mode. - better selectLine. - "o" and "Y" are commands for visual mode, not movements. - simplify find mode within visual mode. - add FindMode binding for "/". --- content_scripts/mode_visual_edit.coffee | 143 ++++++++++++++------------------ 1 file changed, 64 insertions(+), 79 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index d3d51575..f14baff7 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -52,11 +52,12 @@ class CountPrefix extends SuppressPrintable @countPrefixFactor = 1 count -# Some symbolic names for common strings. +# Symbolic names for some common strings. forward = "forward" backward = "backward" character = "character" vimword = "vimword" +lineboundary= "lineboundary" # This implements movement commands with count prefixes for both visual mode and edit mode. class Movement extends CountPrefix @@ -103,10 +104,17 @@ class Movement extends CountPrefix else beforeText[0] + nextCharacterIsWordCharacter: -> + /[A-Za-z0-9_]/.test @nextCharacter() + # Run a movement. For convenience, the following three argument forms are available: # @runMovement "forward word" # @runMovement [ "forward", "word" ] # @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". + # runMovement: (args...) -> # Normalize the various argument forms (to an array of two strings: direction and granularity). movement = @@ -116,16 +124,13 @@ class Movement extends CountPrefix if args.length == 1 then args[0] else args[...2] # Perform the movement. - # We use the pseudo-granularity "vimword" to implement vim's "w" movement (which is not supported - # natively). - if movement[1] == "vimword" and movement[0] == forward - x = @nextCharacter() - if /\s/.test @nextCharacter() - @runMovements [ forward, "word" ], [ backward, "word" ] - else + if movement[1] == vimword and movement[0] == forward + if @nextCharacterIsWordCharacter() @runMovements [ forward, "word" ], [ forward, vimword ] + else + @runMovements [ forward, "word" ], [ backward, "word" ] - else if movement[1] == "vimword" + else if movement[1] == vimword @selection.modify @alterMethod, backward, "word" else @@ -220,8 +225,6 @@ class Movement extends CountPrefix "0": "backward lineboundary" "G": "forward documentboundary" "gg": "backward documentboundary" - "Y": (count) -> @selectLine count, false - "o": -> @reverseSelection() constructor: (options) -> @selection = window.getSelection() @@ -263,6 +266,24 @@ class Movement extends CountPrefix @continueBubbling + # Install basic bindings for find mode, "n" and "N". We do not install these bindings if the is a + # sub-mode of edit mode, because we cannot (yet) guarantee that the selection will remain within the + # active element. + unless @options.parentMode or options.oneMovementOnly + do => + executeFind = (count, findBackwards) => + if query = getFindModeQuery() + initialRange = @selection.getRangeAt(0).cloneRange() + 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 + @scrollIntoView() + break + + @movements.n = (count) -> executeFind count, false + @movements.N = (count) -> executeFind count, true + @movements["/"] = -> enterFindMode() # # End of Movement constructor. @@ -342,7 +363,7 @@ class VisualMode extends Movement when "None" return @changeMode CaretMode when "Caret" - @selection.modify "extend", forward, character + @selection.modify "extend", forward, character unless @options.oneMovementOnly # Yank on . @push @@ -359,20 +380,20 @@ class VisualMode extends Movement @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() @commands.V = -> @changeMode VisualLineMode @commands.c = -> @changeMode CaretMode + @commands.o = -> @reverseSelection() + @commands.Y = (count) -> @selectLine count; @yank() - # Additional commands when run under edit mode (except if only for one movement). - if @options.parentMode and not @options.oneMovementOnly - @commands.x = -> @yank deleteFromDocument: true - @commands.d = -> @yank deleteFromDocument: true - @commands.c = -> - @yank deleteFromDocument: true - @options.parentMode.enterInsertMode() + # Additional commands when run under edit mode. + if @options.parentMode + @commands.x = -> @yank deleteFromDocument: true + @commands.d = -> @yank deleteFromDocument: true + @commands.c = -> @yank deleteFromDocument: true; @options.parentMode.enterInsertMode() # For edit mode's "yy" and "dd". if @options.yankLineCharacter @commands[@options.yankLineCharacter] = (count) -> if @keypressCount == 1 - @selectLine count, true + @selectLine count @yank() # For edit mode's "daw", "cas", and so on. @@ -385,9 +406,6 @@ class VisualMode extends Movement if @keypressCount == 2 @selectLexicalEntity entity, count @yank() - - unless @options.parentMode - @installFindMode() # # End of VisualMode constructor. @@ -399,54 +417,22 @@ class VisualMode extends Movement document.activeElement.blur() super event, target - if @yankedText? and not @options.noCopyToClipboard - console.log "yank:", @yankedText if @debug - @copy @yankedText, true + if @yankedText? + unless @options.noCopyToClipboard + console.log "yank:", @yankedText if @debug + @copy @yankedText, true handleMovementKeyChar: (args...) -> super args... @yank() if @options.oneMovementOnly - selectLine: (count, collapse) -> - @runMovement backward, "lineboundary" - @collapseSelectionToFocus() if collapse - @runMovement forward, "line" for [0...count] - - # This installs a basic binding for find mode, "n" and "N". - # FIXME(smblott). This is a mess, it needs to be reworked. Ideally, incorporate FindMode. - installFindMode: -> - previousFindRange = null - - executeFind = (findBackwards) => - query = getFindModeQuery() - if query - caseSensitive = Utils.hasUpperCase query - @protectClipboard => - initialRange = @selection.getRangeAt(0).cloneRange() - direction = @getDirection() - - # Re-select the previous match, if any; this tells Chrome where to start. - @selectRange previousFindRange if previousFindRange - - window.find query, caseSensitive, findBackwards, true, false, true, false - previousFindRange = newFindRange = @selection.getRangeAt(0).cloneRange() - # FIXME(smblott). What if there are no matches? - - # Install a new range from the original selection anchor to the end of the new match. - range = document.createRange() - which = if direction == forward then "start" else "end" - range.setStart initialRange["#{which}Container"], initialRange["#{which}Offset"] - range.setEnd newFindRange.endContainer, newFindRange.endOffset - @selectRange range - - # If we're now going backwards (or if the selection is empty), then extend the selection to include - # the match itself. - if @getDirection() == backward or @selection.toString().length == 0 - range.setStart newFindRange.startContainer, newFindRange.startOffset - @selectRange range - - @movements.n = (count) -> executeFind false - @movements.N = (count) -> executeFind true + selectLine: (count) -> + @reverseSelection() if @getDirection() == forward + @runMovement backward, lineboundary + @reverseSelection() + @runMovement forward, "line" for [1...count] + @runMovement forward, lineboundary + @runMovement forward, character class VisualLineMode extends VisualMode constructor: (options = {}) -> @@ -461,7 +447,7 @@ class VisualLineMode extends VisualMode extendSelection: -> initialDirection = @getDirection() for direction in [ initialDirection, @opposite[initialDirection] ] - @runMovement direction, "lineboundary" + @runMovement direction, lineboundary @reverseSelection() class CaretMode extends Movement @@ -475,14 +461,13 @@ class CaretMode extends Movement exitOnEscape: true super extend defaults, options - if @selection.type == "None" - @establishInitialSelection() - switch @selection.type when "None" - HUD.showForDuration "Create a selection before entering visual mode.", 2500 - @exit() - return + @establishInitialSelectionAnchor() + if @selection.type == "None" + HUD.showForDuration "Create a selection before entering visual mode.", 2500 + @exit() + return when "Range" @collapseSelectionToFocus() @@ -501,17 +486,17 @@ 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. - establishInitialSelection: -> + establishInitialSelectionAnchor: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT while node = nodes.nextNode() - # Don't pick really short texts; they're likely to be part of a banner. + # Don't choose short text nodes; they're likely to be part of a banner. if node.nodeType == 3 and 50 <= node.data.trim().length element = node.parentElement if DomUtils.getVisibleClientRect(element) and not DomUtils.isEditable element offset = node.data.length - node.data.replace(/^\s+/, "").length range = document.createRange() range.setStart node, offset - range.setEnd node, offset+1 + range.setEnd node, offset @selectRange range return true false @@ -553,7 +538,7 @@ class EditMode extends Movement J: (count) -> for [0...count] - @runMovement forward, "lineboundary" + @runMovement forward, lineboundary @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true, noCopyToClipboard: true DomUtils.simulateTextEntry @element, " " @@ -617,7 +602,7 @@ class EditMode extends Movement # 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 backward, lineboundary @runMovement forward, "line" if direction == forward DomUtils.simulateTextEntry @element, text @runMovement backward, "line" @@ -625,7 +610,7 @@ class EditMode extends Movement DomUtils.simulateTextEntry @element, text openLine: (direction) -> - @runMovement direction, "lineboundary" + @runMovement direction, lineboundary DomUtils.simulateTextEntry @element, "\n" @runMovement backward, character if direction == backward @enterInsertMode() -- cgit v1.2.3 From 0029863f32be7cb079c43faeb9df84b0718f147d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 09:08:26 +0000 Subject: Visual/edit modes: better find-mode integration. --- content_scripts/mode_visual_edit.coffee | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index f14baff7..842f6c0a 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -266,7 +266,7 @@ class Movement extends CountPrefix @continueBubbling - # Install basic bindings for find mode, "n" and "N". We do not install these bindings if the is a + # 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. unless @options.parentMode or options.oneMovementOnly @@ -283,7 +283,9 @@ class Movement extends CountPrefix @movements.n = (count) -> executeFind count, false @movements.N = (count) -> executeFind count, true - @movements["/"] = -> enterFindMode() + @movements["/"] = -> + @findMode = enterFindMode() + @findMode.onExit => new VisualMode # # End of Movement constructor. @@ -361,6 +363,7 @@ class VisualMode extends Movement 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 -- cgit v1.2.3 From e268f1fd57e364feacf372bac92d991fd3abe603 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 09:37:28 +0000 Subject: Visual/edit modes: handle "0" as count prefix, when appropriate. --- content_scripts/mode_visual_edit.coffee | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 842f6c0a..33e854c8 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -221,8 +221,8 @@ class Movement extends CountPrefix "(": "backward sentence" "}": "forward paragraph" "{": "backward paragraph" - "$": "forward lineboundary" "0": "backward lineboundary" + "$": "forward lineboundary" "G": "forward documentboundary" "gg": "backward documentboundary" @@ -255,6 +255,10 @@ class Movement extends CountPrefix @selection = window.getSelection() @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 + if @commands[command] @commands[command].call @, @getCountPrefix() @scrollIntoView() -- cgit v1.2.3 From ec4e2791537c57887471f0d7a6ab1d4e642d1744 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 10:57:28 +0000 Subject: Visual/edit modes: correctly suppress backspace/delete. --- content_scripts/mode_visual_edit.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 33e854c8..f03c01ab 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -16,7 +16,7 @@ class SuppressPrintable extends Mode return @stopBubblingAndTrue if not KeyboardUtils.isPrintable event return @suppressEvent if event.type != "keydown" # Completely suppress Backspace and Delete, they change the selection. - @suppressEvent if event.keyCode in [ 8, 46 ] + return @suppressEvent if event.keyCode in [ keyCodes.backspace, keyCodes.deleteKey ] # Suppress propagation (but not preventDefault) for keydown, printable events. DomUtils.suppressPropagation event @stopBubblingAndFalse -- cgit v1.2.3 From 6624740a639f3bb177a56999227bfcd24aba3712 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 11:35:58 +0000 Subject: Visual/edit modes: another minor code review. --- content_scripts/mode_visual_edit.coffee | 263 ++++++++++++++++++-------------- 1 file changed, 149 insertions(+), 114 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') 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 . @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 -- cgit v1.2.3 From 0df370cead7085eda47778cbb6046969e8bb19b0 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 14:26:02 +0000 Subject: Visual/edit modes: fix bug entering visual mode from edit mode. --- content_scripts/mode_visual_edit.coffee | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 9b39c523..4968fd7f 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -393,14 +393,15 @@ class VisualMode extends Movement exitOnEscape: true super extend defaults, options - 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 + unless @options.oneMovementOnly or options.immediateMovement + 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 . @push -- cgit v1.2.3 From 0949953dbff077f1e6dbddb945e02d390e0eba70 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 15:58:41 +0000 Subject: Visual/edit modes: better "2daw", an so on. --- content_scripts/mode_visual_edit.coffee | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 4968fd7f..c1c62024 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -148,6 +148,7 @@ class Movement extends CountPrefix @selection.modify @alterMethod, backward, word else + console.log movement... @selection.modify @alterMethod, movement... # Return a simple camparable value which depends on various aspects of the selection. This is used to @@ -350,21 +351,22 @@ class Movement extends CountPrefix @exit() @yankedText - # Select a lexical entity, such as a word, or a sentence. The entity should be a movement granularity such - # as "word" or "lineboundary". + # For "daw", "2das", and so on. We select a lexical entity (a word, a sentence or a paragraph). + # Note(smblott). Chrome's paragraph movements are asymmetrical, so we don't support those. So, just words + # and sentences, for now. selectLexicalEntity: (entity, count = 1) -> - # Locate the start of the current entity. - @runMovement forward, entity - @runMovement backward, entity - @collapseSelectionToFocus() if @options.oneMovementOnly - # Move over count entities. - for [0...count] - return unless @runMovements [ forward, 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 ] + if entity == word + if @nextCharacterIsWordCharacter() + @runMovements [ forward, character ], [ backward, word ] + @collapseSelectionToFocus() + @runMovements ([0...count].map -> [ forward, word ])..., [ forward, word ], [ backward, word ] + else + @runMovements [ forward, word ], [ backward, word ], ([0...count].map -> [ forward, word ])... + else if entity == sentence + @runMovement forward, character + @runMovement backward, sentence + @collapseSelectionToFocus() + @runMovements ([0...count].map -> [ forward, sentence ])... # Try to scroll the focus into view. scrollIntoView: -> @@ -436,7 +438,7 @@ class VisualMode extends Movement # For edit mode's "daw", "cas", and so on. if @options.oneMovementOnly @commands.a = (count) -> - for entity in [ word, sentence, paragraph ] + for entity in [ word, sentence ] # , paragraph ] # Note(smblott). Paragraphs don't work. do (entity) => @commands[entity.charAt 0] = -> @selectLexicalEntity entity, count; @yank() -- cgit v1.2.3 From 3a7ebfda0a4a0fa483c7a314ccf0a29c73f27dd4 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 16:02:48 +0000 Subject: Visual/edit modes: better "yy", "dd", an so on. --- content_scripts/mode_visual_edit.coffee | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index c1c62024..2180a922 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -148,7 +148,6 @@ class Movement extends CountPrefix @selection.modify @alterMethod, backward, word else - console.log movement... @selection.modify @alterMethod, movement... # Return a simple camparable value which depends on various aspects of the selection. This is used to @@ -468,7 +467,7 @@ class VisualMode extends Movement @reverseSelection() @runMovement forward, line for [1...count] @runMovement forward, lineboundary - @runMovement forward, character + @runMovement forward, character unless @nextCharacterIsWordCharacter() class VisualLineMode extends VisualMode constructor: (options = {}) -> -- cgit v1.2.3 From 24edba31e73e3155a42f6619b85e5d31ac69cb69 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 30 Jan 2015 16:59:48 +0000 Subject: Visual/edit modes: re-introduce "dap". --- content_scripts/mode_visual_edit.coffee | 39 ++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 6 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 2180a922..cbc60af6 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -105,7 +105,7 @@ class Movement extends CountPrefix new mode # Return the character following the focus, and leave the selection unchanged. - nextCharacter: -> + getNextForwardCharacter: -> beforeText = @selection.toString() if beforeText.length == 0 or @getDirection() == forward @selection.modify "extend", forward, character @@ -116,10 +116,22 @@ class Movement extends CountPrefix else beforeText[0] + # Return the character preceding the focus, and leave the selection unchanged. + getNextBackwardCharacter: -> + beforeText = @selection.toString() + if beforeText.length == 0 or @getDirection() == backward + @selection.modify "extend", backward, character + afterText = @selection.toString() + if beforeText != afterText + @selection.modify "extend", forward, character + afterText[0] + else + beforeText[beforeText.length - 1] + # 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() + -> regexp.test @getNextForwardCharacter() # Run a movement. For convenience, the following three argument forms are available: # @runMovement "forward word" @@ -350,9 +362,10 @@ class Movement extends CountPrefix @exit() @yankedText - # For "daw", "2das", and so on. We select a lexical entity (a word, a sentence or a paragraph). - # Note(smblott). Chrome's paragraph movements are asymmetrical, so we don't support those. So, just words - # and sentences, for now. + # For "daw", "das", and so on. We select a lexical entity (a word, a sentence or a paragraph). + # Note(smblott). It would be nice if the entities could be handled symmetrically. Unfortunately, they + # cannot, and we have to handle each case individually. + # Note(smblott). We currently ignore count. selectLexicalEntity: (entity, count = 1) -> if entity == word if @nextCharacterIsWordCharacter() @@ -366,6 +379,20 @@ class Movement extends CountPrefix @runMovement backward, sentence @collapseSelectionToFocus() @runMovements ([0...count].map -> [ forward, sentence ])... + else if entity == paragraph + # Chrome's paragraph movements are weird: they're not symmetrical, and they tend to stop in odd places + # (like mid-paragraph, for example). Here, we define a paragraph as a new-line delimited entity, + # including the terminating newline. + char = @getNextBackwardCharacter() + while char and char != "\n" + return unless @runMovements [ backward, character ], [ backward, lineboundary ] + char = @getNextBackwardCharacter() + @collapseSelectionToFocus() + char = @getNextForwardCharacter() + while char and char != "\n" + return unless @runMovements [ forward, character ], [ forward, lineboundary ] + char = @getNextForwardCharacter() + @runMovement forward, character # Try to scroll the focus into view. scrollIntoView: -> @@ -437,7 +464,7 @@ class VisualMode extends Movement # For edit mode's "daw", "cas", and so on. if @options.oneMovementOnly @commands.a = (count) -> - for entity in [ word, sentence ] # , paragraph ] # Note(smblott). Paragraphs don't work. + for entity in [ word, sentence, paragraph ] do (entity) => @commands[entity.charAt 0] = -> @selectLexicalEntity entity, count; @yank() -- cgit v1.2.3 From 0c9b69a319e272727474ab6e1dabb3461927323d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 31 Jan 2015 06:16:46 +0000 Subject: Visual/edit modes: rework lexical selection... - And drop "J". --- content_scripts/mode_visual_edit.coffee | 71 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 36 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index cbc60af6..a7abd314 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,15 +1,10 @@ # Todo: # Konami code? -# Use find as a mode. -# Exit on Ctrl-Enter. # Scroll is broken (again). Seems to be after dd. # 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. @@ -347,6 +342,7 @@ class Movement extends CountPrefix # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. yank: (args = {}) -> @yankedText = @selection.toString() + console.log "text:", @yankedText if @options.deleteFromDocument or args.deleteFromDocument @selection.deleteFromDocument() @@ -363,36 +359,39 @@ class Movement extends CountPrefix @yankedText # For "daw", "das", and so on. We select a lexical entity (a word, a sentence or a paragraph). - # Note(smblott). It would be nice if the entities could be handled symmetrically. Unfortunately, they + # Note(smblott). It would be better if the entities could be handled symmetrically. Unfortunately, they # cannot, and we have to handle each case individually. - # Note(smblott). We currently ignore count. selectLexicalEntity: (entity, count = 1) -> - if entity == word - if @nextCharacterIsWordCharacter() - @runMovements [ forward, character ], [ backward, word ] + + switch entity + when word + if @nextCharacterIsWordCharacter() + @runMovements [ forward, character ], [ backward, word ] + @collapseSelectionToFocus() + @runMovements ([0...count].map -> [ forward, word ])..., [ forward, word ], [ backward, word ] + else + @runMovements [ forward, word ], [ backward, word ], ([0...count].map -> [ forward, word ])... + + when sentence + @runMovements [ forward, character ], [ backward, sentence ] @collapseSelectionToFocus() - @runMovements ([0...count].map -> [ forward, word ])..., [ forward, word ], [ backward, word ] - else - @runMovements [ forward, word ], [ backward, word ], ([0...count].map -> [ forward, word ])... - else if entity == sentence - @runMovement forward, character - @runMovement backward, sentence - @collapseSelectionToFocus() - @runMovements ([0...count].map -> [ forward, sentence ])... - else if entity == paragraph - # Chrome's paragraph movements are weird: they're not symmetrical, and they tend to stop in odd places - # (like mid-paragraph, for example). Here, we define a paragraph as a new-line delimited entity, - # including the terminating newline. - char = @getNextBackwardCharacter() - while char and char != "\n" - return unless @runMovements [ backward, character ], [ backward, lineboundary ] + @runMovements ([0...count].map -> [ forward, sentence ])... + + when paragraph + # Chrome's paragraph movements are weird: they're not symmetrical, and tend to stop in odd places + # (like mid-paragraph, for example). Here, we define a paragraph as a new-line delimited entity, + # including the terminating newline. + # Note(smblott). This does not currently use the count. char = @getNextBackwardCharacter() - @collapseSelectionToFocus() - char = @getNextForwardCharacter() - while char and char != "\n" - return unless @runMovements [ forward, character ], [ forward, lineboundary ] + while char? and char != "\n" + return unless @runMovements [ backward, character ], [ backward, lineboundary ] + char = @getNextBackwardCharacter() + @collapseSelectionToFocus() char = @getNextForwardCharacter() - @runMovement forward, character + while char? and char != "\n" + @runMovements [ forward, character ], [ forward, lineboundary ] + char = @getNextForwardCharacter() + @runMovement forward, character # Try to scroll the focus into view. scrollIntoView: -> @@ -589,7 +588,6 @@ 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 @@ -600,11 +598,12 @@ class EditMode extends Movement D: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true C: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() - J: (count) -> - for [0...count] - @runMovement forward, lineboundary - @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true, noCopyToClipboard: true - DomUtils.simulateTextEntry @element, " " + # Disabled. Doesn't work. + # J: (count) -> + # for [0...count] + # @runMovement forward, lineboundary + # @enterVisualModeForMovement 1, immediateMovement: "w", deleteFromDocument: true, noCopyToClipboard: true + # DomUtils.simulateTextEntry @element, " " r: (count) -> handlerStack.push -- cgit v1.2.3 From 6cff1b0dfccdb137e95e52c9a931fed14cec82d5 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 31 Jan 2015 12:30:13 +0000 Subject: Visual/edit modes: change visual-mode start up... When visual mode launches and there *is* a selection but it's outside of the viewport, instead of scrolling it into view, ignore it, and start with a visible caret. --- content_scripts/mode_visual_edit.coffee | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index a7abd314..d936ac1e 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -424,9 +424,20 @@ class VisualMode extends Movement 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 + else + if @selection.type in [ "Caret", "Range" ] + elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward + if DomUtils.getVisibleClientRect elementWithFocus + if @selection.type == "Caret" + # Make the selection visible. + @extendByOneCharacter(forward) or @extendByOneCharacter backward + else + # If the selection is outside of the viewport, we clear it. We guess that the user has moved on, + # and is more likely to be interested in visible content. + @selection.removeAllRanges() + + if @selection.type != "Range" + HUD.showForDuration "Entering caret mode first..", 2500 @changeMode CaretMode return @@ -530,7 +541,7 @@ class CaretMode extends Movement @exit() return when "Range" - @collapseSelectionToFocus() + @collapseSelectionToAnchor() @selection.modify "extend", forward, character @scrollIntoView() -- cgit v1.2.3 From 11199de98a2c29601e38e75af561057ab55a0a83 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 31 Jan 2015 13:01:18 +0000 Subject: Visual/edit modes: change implementation of vimword. --- content_scripts/mode_visual_edit.coffee | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index d936ac1e..253d892a 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -124,8 +124,10 @@ class Movement extends CountPrefix beforeText[beforeText.length - 1] # Test whether the character following the focus is a word character. Leave the selection unchanged. + # We include "." as a word character here. With this, "w" sometimes jumps one word too far. However, it's + # better than leaving it out, in which case "w" jumps backwards! nextCharacterIsWordCharacter: do -> - regexp = /[A-Za-z0-9_]/ + regexp = /[A-Za-z0-9_]|\./ -> regexp.test @getNextForwardCharacter() # Run a movement. For convenience, the following three argument forms are available: @@ -147,7 +149,7 @@ 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, word ], [ backward, word ] else @runMovements [ forward, word ], [ backward, word ] -- cgit v1.2.3 From d03ba099fcbe5e07dfdfa454afdb86fd5aad2c89 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 31 Jan 2015 13:34:36 +0000 Subject: Visual/edit modes: character-by-character word movements. --- content_scripts/mode_visual_edit.coffee | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 253d892a..a170a2da 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -124,10 +124,8 @@ class Movement extends CountPrefix beforeText[beforeText.length - 1] # Test whether the character following the focus is a word character. Leave the selection unchanged. - # We include "." as a word character here. With this, "w" sometimes jumps one word too far. However, it's - # better than leaving it out, in which case "w" jumps backwards! nextCharacterIsWordCharacter: do -> - regexp = /[A-Za-z0-9_]|\./ + regexp = /[A-Za-z0-9_]/ -> regexp.test @getNextForwardCharacter() # Run a movement. For convenience, the following three argument forms are available: @@ -146,16 +144,24 @@ class Movement extends CountPrefix else if args.length == 1 then args[0] else args[...2] - # Perform the movement. + # Word movements are different on Linux and Windows, see #1441. So we implement some of them + # character-by-character. if movement[1] == vimword and movement[0] == forward - if @nextCharacterIsWordCharacter() - @runMovements [ forward, word ], [ forward, word ], [ backward, word ] - else - @runMovements [ forward, word ], [ backward, word ] + while @nextCharacterIsWordCharacter() + return unless @runMovements [ forward, character ] + while @getNextForwardCharacter() and not @nextCharacterIsWordCharacter() + return unless @runMovements [ forward, character ] else if movement[1] == vimword @selection.modify @alterMethod, backward, word + # As above, we implement this character-by-character to get consistent behavior on Windows and Linux. + if movement[1] == word and movement[0] == forward + while @getNextForwardCharacter() and not @nextCharacterIsWordCharacter() + return unless @runMovements [ forward, character ] + while @nextCharacterIsWordCharacter() + return unless @runMovements [ forward, character ] + else @selection.modify @alterMethod, movement... -- cgit v1.2.3 From 847aca4859567aaa88e19d41139d236102bc2024 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sat, 31 Jan 2015 14:25:47 +0000 Subject: Visual/edit modes: exit visual mode on click in input element. --- content_scripts/mode_visual_edit.coffee | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index a170a2da..00c842ed 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -449,15 +449,20 @@ class VisualMode extends Movement @changeMode CaretMode return - # Yank on . @push - _name: "#{@id}/enter" + _name: "#{@id}/enter/click" + # Yank on . keypress: (event) => if event.keyCode == keyCodes.enter unless event.metaKey or event.ctrlKey or event.altKey or event.shiftKey @yank() return @suppressEvent @continueBubbling + # Click in a focusable element exits. + click: (event) => + @alwaysContinueBubbling => + unless @options.parentMode + @exit event, event.target if DomUtils.isFocusable event.target # Visual-mode commands. unless @options.oneMovementOnly @@ -494,7 +499,7 @@ class VisualMode extends Movement # Don't leave the user in insert mode just because they happen to have selected text within an input # element. if document.activeElement and DomUtils.isEditable document.activeElement - document.activeElement.blur() + document.activeElement.blur() unless event?.type == "click" super event, target if @yankedText? @@ -554,6 +559,13 @@ class CaretMode extends Movement @selection.modify "extend", forward, character @scrollIntoView() + @push + _name: "#{@id}/click" + # Click in a focusable element exits. + click: (event) => + @alwaysContinueBubbling => + @exit event, event.target if DomUtils.isFocusable event.target + extend @commands, v: -> @changeMode VisualMode V: -> @changeMode VisualLineMode -- cgit v1.2.3 From 33f0306b8f63e56ab56e30e8177ffaea6298f35c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 1 Feb 2015 08:03:20 +0000 Subject: Visual/edit modes: yet another code review. --- content_scripts/mode_visual_edit.coffee | 158 ++++++++++++++++++-------------- 1 file changed, 87 insertions(+), 71 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 00c842ed..8c82205e 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -1,13 +1,32 @@ -# Todo: -# Konami code? -# Scroll is broken (again). Seems to be after dd. -# Paste of whole lines. -# Arrow keys. -# J - -# This prevents printable characters from being passed through to the underlying page. It should, however, -# allow through Chrome keyboard shortcuts. +# +# The main modes defined here are: +# - VisualMode +# - VisualLineMode +# - CaretMode +# - EditMode (experimental) +# +# SuppressPrintable and CountPrefix are shared utility base classes. +# Movement is a shared vim-like movement base class. +# +# The inheritance hierarchy is: +# - Mode, SuppressPrintable, CountPrefix, Movement, [ VisualMode | CaretMode | EditMode ] +# - Mode, SuppressPrintable, CountPrefix, Movement, VisualMode, VisualLineMode +# +# The possible mode states are: +# - ..., VisualMode +# - ..., VisualLineMode +# - ..., CaretMode +# - ..., VisualMode, FindMode +# - ..., VisualLineMode, FindMode +# - ..., CaretMode, FindMode +# - ..., EditMode +# - ..., EditMode, InsertMode +# - ..., EditMode, VisualMode +# - ..., EditMode, VisualLineMode +# + +# This prevents printable characters from being passed through to underlying modes or to the underlying page. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => @@ -19,15 +38,13 @@ class SuppressPrintable extends Mode DomUtils.suppressPropagation event @stopBubblingAndFalse - super extend options, - keydown: handler - keypress: handler - keyup: handler + super extend options, keydown: handler, keypress: handler, keyup: handler -# This watches keypresses and maintains the count prefix as number keys and other keys are pressed. +# This monitors keypresses and maintains the count prefix. class CountPrefix extends SuppressPrintable constructor: (options) -> @countPrefix = "" + # This allows us to implement both "d3w" and "3dw". Also, "3d2w" deletes six words. @countPrefixFactor = options.initialCountPrefix || 1 super options @@ -43,7 +60,6 @@ class CountPrefix extends SuppressPrintable else "" - # This handles both "d3w" and "3dw". Also, "3d2w" deletes six words. getCountPrefix: -> count = @countPrefixFactor * if 0 < @countPrefix?.length then parseInt @countPrefix else 1 @countPrefix = "" @@ -61,7 +77,7 @@ paragraph = "paragraph" vimword = "vimword" lineboundary= "lineboundary" -# This implements movement commands with count prefixes for both visual mode and edit mode. +# This implements vim-like movements, and includes quite a number of gereral utility methods. class Movement extends CountPrefix opposite: forward: backward, backward: forward @@ -71,16 +87,14 @@ 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 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. + # However, @protectClipboard may later try to restore the original clipboard contents. Therefore, we + # disable copy so that subsequent copies do not propagate. @copy = (->) if isFinalUserCopy # 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. + # 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 @@ -97,9 +111,10 @@ class Movement extends CountPrefix if @options.parentMode @options.parentMode.launchSubMode mode, options else - new mode + new mode options - # Return the character following the focus, and leave the selection unchanged. + # Return the character following (to the right of) the focus, and leave the selection unchanged. Returns + # undefined if there is no such character. getNextForwardCharacter: -> beforeText = @selection.toString() if beforeText.length == 0 or @getDirection() == forward @@ -111,7 +126,7 @@ class Movement extends CountPrefix else beforeText[0] - # Return the character preceding the focus, and leave the selection unchanged. + # As above, but backwards. getNextBackwardCharacter: -> beforeText = @selection.toString() if beforeText.length == 0 or @getDirection() == backward @@ -125,10 +140,10 @@ class Movement extends CountPrefix # Test whether the character following the focus is a word character. Leave the selection unchanged. nextCharacterIsWordCharacter: do -> - regexp = /[A-Za-z0-9_]/ - -> regexp.test @getNextForwardCharacter() + regexp = /[A-Za-z0-9_]/; -> regexp.test @getNextForwardCharacter() - # Run a movement. For convenience, the following three argument forms are available: + # Run a movement. This is the core movement method, all movements happen here. For convenience, the + # following three argument forms are supported: # @runMovement "forward word" # @runMovement [ "forward", "word" ] # @runMovement "forward", "word" @@ -137,33 +152,33 @@ class Movement extends CountPrefix # 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). - movement = + # Normalize the various argument forms. + [ direction, granularity ] = if typeof(args[0]) == "string" and args.length == 1 args[0].trim().split /\s+/ else if args.length == 1 then args[0] else args[...2] - # Word movements are different on Linux and Windows, see #1441. So we implement some of them + # Native word movements behave differently on Linux and Windows, see #1441. So we implement some of them # character-by-character. - if movement[1] == vimword and movement[0] == forward + if granularity == vimword and direction == forward while @nextCharacterIsWordCharacter() return unless @runMovements [ forward, character ] while @getNextForwardCharacter() and not @nextCharacterIsWordCharacter() return unless @runMovements [ forward, character ] - else if movement[1] == vimword + else if granularity == vimword @selection.modify @alterMethod, backward, word # As above, we implement this character-by-character to get consistent behavior on Windows and Linux. - if movement[1] == word and movement[0] == forward + if granularity == word and direction == forward while @getNextForwardCharacter() and not @nextCharacterIsWordCharacter() return unless @runMovements [ forward, character ] while @nextCharacterIsWordCharacter() return unless @runMovements [ forward, character ] else - @selection.modify @alterMethod, movement... + @selection.modify @alterMethod, direction, granularity # 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. @@ -185,8 +200,8 @@ class Movement extends CountPrefix # 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() + element = document.activeElement 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 here because the normal method (below) does not work for simple text inputs. @@ -202,7 +217,7 @@ class Movement extends CountPrefix which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Try to extend the selection one character in "direction". Return 1, -1 or 0, indicating whether the + # Try to extend the selection one character in direction. Return 1, -1 or 0, indicating whether the # selection got bigger, or smaller, or is unchanged. extendByOneCharacter: (direction) -> length = @selection.toString().length @@ -211,8 +226,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 (although that probably - # won't work for text inputs). + # NOTE(smblott). This could be better, see: https://dom.spec.whatwg.org/#interface-range (haowever, that probably + # wouldn'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 ] @@ -233,7 +248,7 @@ class Movement extends CountPrefix @selection.removeAllRanges() @selection.addRange range - # A movement can be a string (which will be passed to @runMovement() count times), or a function (which will + # 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). movements: "l": "forward character" @@ -253,29 +268,30 @@ class Movement extends CountPrefix "gg": "backward documentboundary" "Y": (count) -> @selectLine count; @yank() + # This handles a movement, but protects to selection while doing so. runMovementKeyChar: (args...) -> @protectClipboard => @handleMovementKeyChar args... - # Handle a single movement keyChar. This is extended by super-classes. + # Handle a single movement keyChar. This is extended (wrapped) 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: + # The bahavior of Movement can be tweaked by setting the following options: # - options.parentMode (a mode) - # This instance is a sub-mode of edit mode. + # This instance is a sub-mode of another mode (currently, only 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, after which it yanks and exits. + # - options.immediateMovement (a keyChar 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. + # If truthy, then do not copy the yanked text to the clipboard when yanking. # constructor: (options) -> @selection = window.getSelection() @@ -331,9 +347,8 @@ class Movement extends CountPrefix initialRange = @selection.getRangeAt(0).cloneRange() for [0...count] unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false - HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 @setSelectionRange initialRange - @scrollIntoView() + HUD.showForDuration "No matches.", 1500 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. @@ -347,10 +362,10 @@ class Movement extends CountPrefix # # End of Movement constructor. - # Yank the selection; always exits; either deletes the selection or collapses it; returns the yanked text. + # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and + # returns it. yank: (args = {}) -> @yankedText = @selection.toString() - console.log "text:", @yankedText if @options.deleteFromDocument or args.deleteFromDocument @selection.deleteFromDocument() @@ -376,9 +391,7 @@ class Movement extends CountPrefix if @nextCharacterIsWordCharacter() @runMovements [ forward, character ], [ backward, word ] @collapseSelectionToFocus() - @runMovements ([0...count].map -> [ forward, word ])..., [ forward, word ], [ backward, word ] - else - @runMovements [ forward, word ], [ backward, word ], ([0...count].map -> [ forward, word ])... + @runMovements ([0...count].map -> [ forward, vimword ])... when sentence @runMovements [ forward, character ], [ backward, sentence ] @@ -397,7 +410,7 @@ class Movement extends CountPrefix @collapseSelectionToFocus() char = @getNextForwardCharacter() while char? and char != "\n" - @runMovements [ forward, character ], [ forward, lineboundary ] + return unless @runMovements [ forward, character ], [ forward, lineboundary ] char = @getNextForwardCharacter() @runMovement forward, character @@ -407,7 +420,7 @@ class Movement extends CountPrefix if @element and DomUtils.isEditable @element if @element.clientHeight < @element.scrollHeight if @element.isContentEditable - # Help(smblott). 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 @@ -418,7 +431,6 @@ class Movement extends CountPrefix class VisualMode extends Movement constructor: (options = {}) -> - @selection = window.getSelection() @alterMethod = "extend" defaults = @@ -428,6 +440,7 @@ class VisualMode extends Movement exitOnEscape: true super extend defaults, options + # Establish or use the initial selection. If that's not possible, then enter caret mode. unless @options.oneMovementOnly or options.immediateMovement if @options.parentMode and @selection.type == "Caret" # We're being called from edit mode, so establish an intial visible selection. @@ -437,15 +450,15 @@ class VisualMode extends Movement elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward if DomUtils.getVisibleClientRect elementWithFocus if @selection.type == "Caret" - # Make the selection visible. + # The caret is in the viewport. Make make it visible. @extendByOneCharacter(forward) or @extendByOneCharacter backward else - # If the selection is outside of the viewport, we clear it. We guess that the user has moved on, - # and is more likely to be interested in visible content. + # The selection is outside of the viewport: clear it. We guess that the user has moved on, and is + # more likely to be interested in visible content. @selection.removeAllRanges() if @selection.type != "Range" - HUD.showForDuration "Entering caret mode first..", 2500 + HUD.showForDuration "No usable selection, entering caret mode...", 2500 @changeMode CaretMode return @@ -517,7 +530,8 @@ class VisualMode extends Movement @reverseSelection() @runMovement forward, line for [1...count] @runMovement forward, lineboundary - @runMovement forward, character unless @nextCharacterIsWordCharacter() + # Include the next character if it is a newline. + @runMovement forward, character if @getNextForwardCharacter() == "\n" class VisualLineMode extends VisualMode constructor: (options = {}) -> @@ -546,6 +560,7 @@ class CaretMode extends Movement exitOnEscape: true super extend defaults, options + # Establish the initial caret. switch @selection.type when "None" @establishInitialSelectionAnchor() @@ -566,6 +581,7 @@ class CaretMode extends Movement @alwaysContinueBubbling => @exit event, event.target if DomUtils.isFocusable event.target + # Commands to exit caret mode, and enter visual mode. extend @commands, v: -> @changeMode VisualMode V: -> @changeMode VisualLineMode @@ -578,6 +594,8 @@ 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 big enough to be interesting. + # TODO(smblott). It might be better to do something similar to Clearly or Readability; that is, try to find + # the start of the page's main textual content. establishInitialSelectionAnchor: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT while node = nodes.nextNode() @@ -596,8 +614,8 @@ class CaretMode extends Movement class EditMode extends Movement constructor: (options = {}) -> - @element = document.activeElement @alterMethod = "move" + @element = document.activeElement return unless @element and DomUtils.isEditable @element defaults = @@ -607,6 +625,7 @@ class EditMode extends Movement exitOnBlur: @element super extend defaults, options + # Edit mode commands. extend @commands, i: -> @enterInsertMode() a: -> @enterInsertMode() @@ -629,7 +648,10 @@ class EditMode extends Movement D: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true C: (count) -> @enterVisualModeForMovement 1, immediateMovement: "$", deleteFromDocument: true, onYank: => @enterInsertMode() - # Disabled. Doesn't work. + '~': (count) -> @swapCase count, true + 'g~': (count) -> @swapCase count, false + + # Disabled. Doesn't work reliably. # J: (count) -> # for [0...count] # @runMovement forward, lineboundary @@ -648,10 +670,7 @@ class EditMode extends Movement DomUtils.simulateTextEntry @element, [0...count].map(-> keyChar).join "" @suppressEvent - '~': (count) -> @swapCase count, true - 'g~': (count) -> @swapCase count, false - - # Disabled as potentially confusing. + # Disabled: potentially confusing. # # If the input is empty, then enter insert mode immediately. # unless @element.isContentEditable # if @element.value.trim() == "" @@ -669,10 +688,7 @@ class EditMode extends Movement onYank: (text) => chars = for char in text.split "" - if char == char.toLowerCase() - char.toUpperCase() - else - char.toLowerCase() + if char == char.toLowerCase() then char.toUpperCase() else char.toLowerCase() DomUtils.simulateTextEntry @element, chars.join "" # For "p" and "P". -- cgit v1.2.3 From bc17113b52da8d234a68b7e34b755197989d8a4c Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 1 Feb 2015 11:49:24 +0000 Subject: Visual/edit modes: fine-tune entry/exit logic. - When the user exits visual mode via escape, we reinstall the original selection (from launch). --- content_scripts/mode_visual_edit.coffee | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 8c82205e..e7d79e7e 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -9,7 +9,7 @@ # SuppressPrintable and CountPrefix are shared utility base classes. # Movement is a shared vim-like movement base class. # -# The inheritance hierarchy is: +# The class inheritance hierarchy is: # - Mode, SuppressPrintable, CountPrefix, Movement, [ VisualMode | CaretMode | EditMode ] # - Mode, SuppressPrintable, CountPrefix, Movement, VisualMode, VisualLineMode # @@ -111,7 +111,7 @@ class Movement extends CountPrefix if @options.parentMode @options.parentMode.launchSubMode mode, options else - new mode options + new mode extend options, initialRangeOnLaunch: @initialRangeOnLaunch # Return the character following (to the right of) the focus, and leave the selection unchanged. Returns # undefined if there is no such character. @@ -309,6 +309,11 @@ class Movement extends CountPrefix @runMovementKeyChar @options.immediateMovement, @getCountPrefix() return + # Track the initial selection range. We'll restore it if the user exits with Escape. + unless @options.parentMode + @initialRangeOnLaunch = options.initialRangeOnLaunch || + (if @selection.type == "None" then "None" else @selection.getRangeAt 0) + # This is the main keyboard-event handler for movements and commands. @push _name: "#{@id}/keypress" @@ -348,7 +353,7 @@ class Movement extends CountPrefix for [0...count] unless window.find query, Utils.hasUpperCase(query), findBackwards, true, false, true, false @setSelectionRange initialRange - HUD.showForDuration "No matches.", 1500 + HUD.showForDuration("No matches for '" + query + "'", 1000) 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. @@ -358,10 +363,22 @@ class Movement extends CountPrefix @movements.N = (count) -> executeFind count, true @movements["/"] = -> @findMode = enterFindMode() - @findMode.onExit => new VisualMode + @findMode.onExit => @changeMode VisualMode # # End of Movement constructor. + exit: (event, target) -> + super event, target + + unless @options.parentMode + # When the user exits via Escape, we reinstall the pre-launch selection (or, if there was none, just + # collapse the selection to the current anchor). + if @initialRangeOnLaunch? and event?.type == "keydown" and KeyboardUtils.isEscape event + if @initialRangeOnLaunch == "None" + @collapseSelectionToAnchor() + else + @setSelectionRange @initialRangeOnLaunch unless @initialRangeOnLaunch == "None" + # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and # returns it. yank: (args = {}) -> -- cgit v1.2.3 From b5a6dfa2a6665f7e028e50cdf7c5e47a7f58d70d Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Sun, 1 Feb 2015 14:45:49 +0000 Subject: Visual/edit modes: tweaks... - Collapse to focus when entering caret mode from visual mode. --- content_scripts/mode_visual_edit.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index e7d79e7e..82e2b189 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -500,7 +500,7 @@ class VisualMode extends Movement @commands.p = -> chrome.runtime.sendMessage handler: "openUrlInCurrentTab", url: @yank() @commands.P = -> chrome.runtime.sendMessage handler: "openUrlInNewTab", url: @yank() @commands.V = -> @changeMode VisualLineMode - @commands.c = -> @changeMode CaretMode + @commands.c = -> @collapseSelectionToFocus(); @changeMode CaretMode @commands.o = -> @reverseSelection() # Additional commands when run under edit mode. -- cgit v1.2.3 From edfe3d0ce2726e90559b200f607dc0dec48526b9 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 2 Feb 2015 08:27:27 +0000 Subject: Visual/edit modes: WIP, scrolling content editable. --- content_scripts/mode_visual_edit.coffee | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 82e2b189..749559f7 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -437,7 +437,15 @@ class Movement extends CountPrefix if @element and DomUtils.isEditable @element if @element.clientHeight < @element.scrollHeight if @element.isContentEditable - # Help(smblott)! How do we do this? This case matters for Gmail and Google's inbox. + # WIP... + elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward + console.log elementWithFocus.innerHTML + position = elementWithFocus.getClientRects()[0].top - @element.getClientRects()[0].top + console.log "top", position + Scroller.scrollToPosition @element, position, 0 + position = elementWithFocus.getClientRects()[0].bottom - @element.getClientRects()[0].top + console.log "bottom", position + Scroller.scrollToPosition @element, position, 0 else position = if @getDirection() == backward then @element.selectionStart else @element.selectionEnd coords = DomUtils.getCaretCoordinates @element, position -- cgit v1.2.3 From 0028886ea455ef09386ff822727fd666c666da35 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 2 Feb 2015 09:40:54 +0000 Subject: Visual/edit modes: escape in visual mode clears selection. --- content_scripts/mode_visual_edit.coffee | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 749559f7..0bc0639e 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -111,7 +111,7 @@ class Movement extends CountPrefix if @options.parentMode @options.parentMode.launchSubMode mode, options else - new mode extend options, initialRangeOnLaunch: @initialRangeOnLaunch + new mode options # Return the character following (to the right of) the focus, and leave the selection unchanged. Returns # undefined if there is no such character. @@ -309,11 +309,6 @@ class Movement extends CountPrefix @runMovementKeyChar @options.immediateMovement, @getCountPrefix() return - # Track the initial selection range. We'll restore it if the user exits with Escape. - unless @options.parentMode - @initialRangeOnLaunch = options.initialRangeOnLaunch || - (if @selection.type == "None" then "None" else @selection.getRangeAt 0) - # This is the main keyboard-event handler for movements and commands. @push _name: "#{@id}/keypress" @@ -370,15 +365,6 @@ class Movement extends CountPrefix exit: (event, target) -> super event, target - unless @options.parentMode - # When the user exits via Escape, we reinstall the pre-launch selection (or, if there was none, just - # collapse the selection to the current anchor). - if @initialRangeOnLaunch? and event?.type == "keydown" and KeyboardUtils.isEscape event - if @initialRangeOnLaunch == "None" - @collapseSelectionToAnchor() - else - @setSelectionRange @initialRangeOnLaunch unless @initialRangeOnLaunch == "None" - # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and # returns it. yank: (args = {}) -> -- cgit v1.2.3 From 03f764098aeef114679facfc22104faf141cf3c1 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 2 Feb 2015 14:59:14 +0000 Subject: Visual/edit modes: WIP, scrolling content editable. --- content_scripts/mode_visual_edit.coffee | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 0bc0639e..69d4a0a6 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -426,11 +426,10 @@ class Movement extends CountPrefix # WIP... elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward console.log elementWithFocus.innerHTML - position = elementWithFocus.getClientRects()[0].top - @element.getClientRects()[0].top - console.log "top", position - Scroller.scrollToPosition @element, position, 0 - position = elementWithFocus.getClientRects()[0].bottom - @element.getClientRects()[0].top - console.log "bottom", position + # position = @element.getClientRects()[0].top - elementWithFocus.getClientRects()[0].top + # console.log "top", position + # Scroller.scrollToPosition @element, position, 0 + position = elementWithFocus.getClientRects()[0].bottom - @element.getClientRects()[0].top - @element.clientHeight + @element.scrollTop Scroller.scrollToPosition @element, position, 0 else position = if @getDirection() == backward then @element.selectionStart else @element.selectionEnd -- cgit v1.2.3 From 95e086563b918364d3038f6489cc97c73fcb7180 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Mon, 2 Feb 2015 16:56:07 +0000 Subject: Visual/edit modes: Escape-Escape clears selection. --- content_scripts/mode_visual_edit.coffee | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 69d4a0a6..caecfbde 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -362,9 +362,6 @@ class Movement extends CountPrefix # # End of Movement constructor. - exit: (event, target) -> - super event, target - # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and # returns it. yank: (args = {}) -> @@ -384,6 +381,26 @@ class Movement extends CountPrefix @exit() @yankedText + exit: (event, target) -> + unless @options.parentMode or @options.oneMovementOnly + # If we're exiting on escape and there is a range selection, then we leave it in place. However, an + # immediately-following Escape clears the selection. See #1441. + if @selection.type == "Range" and event?.type == "keydown" and KeyboardUtils.isEscape event + handlerStack.push + _name: "visual/range/escape" + click: -> handlerStack.remove() + focus: -> handlerStack.remove() + keydown: (event) => + handlerStack.remove() + if @selection.type == "Range" and event.type == "keydown" and KeyboardUtils.isEscape event + @collapseSelectionToFocus() + DomUtils.suppressKeyupAfterEscape handlerStack + @suppressEvent + else + @continueBubbling + + super event, target + # For "daw", "das", and so on. We select a lexical entity (a word, a sentence or a paragraph). # Note(smblott). It would be better if the entities could be handled symmetrically. Unfortunately, they # cannot, and we have to handle each case individually. -- cgit v1.2.3 From c1681fea2f2629c6bee1e27c5dfc704d77553d96 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 3 Feb 2015 07:37:54 +0000 Subject: Visual/edit modes: better (?) guessing for caret mode. When caret mode guesses a selection score text nodes by their visible area and the number of non-whitespace characters they contain. Choose the first non-whitespace character in the highest-scoring node. The intention is to avoid making a poor choice for the initial caret position. --- content_scripts/mode_visual_edit.coffee | 39 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index caecfbde..5b622e4d 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -619,25 +619,32 @@ class CaretMode extends Movement @selection.modify "extend", forward, character # 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 big enough to be interesting. - # TODO(smblott). It might be better to do something similar to Clearly or Readability; that is, try to find - # the start of the page's main textual content. + # selection. As a heuristic, we score visible text nodes by their visible area and the number of + # non-whitespace characters they contain. We pick the first non-whitespece character in the highest scoring + # node. establishInitialSelectionAnchor: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT - while node = nodes.nextNode() - # Don't choose short text nodes; they're likely to be part of a banner. - if node.nodeType == 3 and 50 <= node.data.trim().length + + # Find and score candidate text nodes. + candidates = + for node in (n while n = nodes.nextNode()) + continue unless node.nodeType == 3 and 0 < 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 - @setSelectionRange range - return true - false + rect = DomUtils.getVisibleClientRect element + continue unless rect and not DomUtils.isEditable element + area = (rect.bottom - rect.top) * (rect.right - rect.left) + chars = node.data.split(/\s+/).join().length + { node: node, score: chars * area } + + if 0 < candidates.length + node = (candidates.sort (a,b) -> b.score - a.score)[0].node + # 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 + @setSelectionRange range + return range class EditMode extends Movement constructor: (options = {}) -> -- cgit v1.2.3 From 33abff96eeb052af807f9987506d611ce25b2757 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Tue, 3 Feb 2015 08:24:42 +0000 Subject: Visual/edit modes: fix bug in event handling. --- content_scripts/mode_visual_edit.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 5b622e4d..4ab425a3 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -388,8 +388,8 @@ class Movement extends CountPrefix if @selection.type == "Range" and event?.type == "keydown" and KeyboardUtils.isEscape event handlerStack.push _name: "visual/range/escape" - click: -> handlerStack.remove() - focus: -> handlerStack.remove() + click: -> handlerStack.remove(); @continueBubbling + focus: -> handlerStack.remove(); @continueBubbling keydown: (event) => handlerStack.remove() if @selection.type == "Range" and event.type == "keydown" and KeyboardUtils.isEscape event -- cgit v1.2.3 From 49a0f357e4c8b110993200a4340d9c36e9749333 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Wed, 4 Feb 2015 14:18:11 +0000 Subject: Revert "Visual/edit modes: better (?) guessing for caret mode." This reverts commit c1681fea2f2629c6bee1e27c5dfc704d77553d96. --- content_scripts/mode_visual_edit.coffee | 39 ++++++++++++++------------------- 1 file changed, 16 insertions(+), 23 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 4ab425a3..8f4459ee 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -619,32 +619,25 @@ class CaretMode extends Movement @selection.modify "extend", forward, character # When visual mode starts and there's no existing selection, we launch CaretMode and try to establish a - # selection. As a heuristic, we score visible text nodes by their visible area and the number of - # non-whitespace characters they contain. We pick the first non-whitespece character in the highest scoring - # node. + # selection. As a heuristic, we pick the first non-whitespace character of the first visible text node + # which seems to be big enough to be interesting. + # TODO(smblott). It might be better to do something similar to Clearly or Readability; that is, try to find + # the start of the page's main textual content. establishInitialSelectionAnchor: -> nodes = document.createTreeWalker document.body, NodeFilter.SHOW_TEXT - - # Find and score candidate text nodes. - candidates = - for node in (n while n = nodes.nextNode()) - continue unless node.nodeType == 3 and 0 < node.data.trim().length + while node = nodes.nextNode() + # Don't choose short text nodes; they're likely to be part of a banner. + if node.nodeType == 3 and 50 <= node.data.trim().length element = node.parentElement - rect = DomUtils.getVisibleClientRect element - continue unless rect and not DomUtils.isEditable element - area = (rect.bottom - rect.top) * (rect.right - rect.left) - chars = node.data.split(/\s+/).join().length - { node: node, score: chars * area } - - if 0 < candidates.length - node = (candidates.sort (a,b) -> b.score - a.score)[0].node - # 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 - @setSelectionRange range - return range + 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 + @setSelectionRange range + return true + false class EditMode extends Movement constructor: (options = {}) -> -- cgit v1.2.3 From 0ca75980d714f2863ad8f46eb69ccc17e917ede6 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 5 Feb 2015 12:57:24 +0000 Subject: Visual/edit modes: visualmode-escape clears selection. This effectively reverts 95e086563b918364d3038f6489cc97c73fcb7180 (Escape-Escape clears selection) pending discussion of the right UX around exitng visual mode. --- content_scripts/mode_visual_edit.coffee | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 8f4459ee..c62fae87 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -383,21 +383,24 @@ class Movement extends CountPrefix exit: (event, target) -> unless @options.parentMode or @options.oneMovementOnly - # If we're exiting on escape and there is a range selection, then we leave it in place. However, an - # immediately-following Escape clears the selection. See #1441. - if @selection.type == "Range" and event?.type == "keydown" and KeyboardUtils.isEscape event - handlerStack.push - _name: "visual/range/escape" - click: -> handlerStack.remove(); @continueBubbling - focus: -> handlerStack.remove(); @continueBubbling - keydown: (event) => - handlerStack.remove() - if @selection.type == "Range" and event.type == "keydown" and KeyboardUtils.isEscape event - @collapseSelectionToFocus() - DomUtils.suppressKeyupAfterEscape handlerStack - @suppressEvent - else - @continueBubbling + @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event + + # Disabled, pending discussion of fine-tuning the UX. Simpler alternative is implemented above. + # # If we're exiting on escape and there is a range selection, then we leave it in place. However, an + # # immediately-following Escape clears the selection. See #1441. + # if @selection.type == "Range" and event?.type == "keydown" and KeyboardUtils.isEscape event + # handlerStack.push + # _name: "visual/range/escape" + # click: -> handlerStack.remove(); @continueBubbling + # focus: -> handlerStack.remove(); @continueBubbling + # keydown: (event) => + # handlerStack.remove() + # if @selection.type == "Range" and event.type == "keydown" and KeyboardUtils.isEscape event + # @collapseSelectionToFocus() + # DomUtils.suppressKeyupAfterEscape handlerStack + # @suppressEvent + # else + # @continueBubbling super event, target -- cgit v1.2.3 From ae1697b6697e24c77fc852b02c760871db995a3f Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 5 Feb 2015 14:57:32 +0000 Subject: Visual/edit modes: always fully remove the selection on exit from visual mode. There's considerable discussion in #1441 as to what we should do with the selection on leaving visual mode. Good arguments have been made as to why we should keep the selection. However, at this point, keep-it-simple seems like the best strategy, and wholly removing the selection (probably) provides fewer ways for the user to shoot themselves in the foot. --- content_scripts/mode_visual_edit.coffee | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index c62fae87..7223e099 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -366,11 +366,7 @@ class Movement extends CountPrefix # returns it. yank: (args = {}) -> @yankedText = @selection.toString() - - if @options.deleteFromDocument or args.deleteFromDocument - @selection.deleteFromDocument() - else - @collapseSelectionToAnchor() + @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length @@ -382,8 +378,10 @@ class Movement extends CountPrefix @yankedText exit: (event, target) -> - unless @options.parentMode or @options.oneMovementOnly - @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event + @selection.removeAllRanges() unless @options.parentMode or @options.oneMovementOnly + # Disabled. We'll go with always removing the selection (as above), for now. + # unless @options.parentMode or @options.oneMovementOnly + # @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event # Disabled, pending discussion of fine-tuning the UX. Simpler alternative is implemented above. # # If we're exiting on escape and there is a range selection, then we leave it in place. However, an -- cgit v1.2.3 From 449462b2580478b9c6d8b35f05231aad989d01aa Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Thu, 5 Feb 2015 15:03:55 +0000 Subject: Visual/edit modes: Fix ae1697b6697e24c77fc852b02c760871db995a3f... which was broken. --- content_scripts/mode_visual_edit.coffee | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index 7223e099..ba0bc307 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -367,6 +367,7 @@ class Movement extends CountPrefix yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument + @selection.removeAllRanges() message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length @@ -378,10 +379,8 @@ class Movement extends CountPrefix @yankedText exit: (event, target) -> - @selection.removeAllRanges() unless @options.parentMode or @options.oneMovementOnly - # Disabled. We'll go with always removing the selection (as above), for now. - # unless @options.parentMode or @options.oneMovementOnly - # @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event + unless @options.parentMode or @options.oneMovementOnly + @selection.removeAllRanges() if event?.type == "keydown" and KeyboardUtils.isEscape event # Disabled, pending discussion of fine-tuning the UX. Simpler alternative is implemented above. # # If we're exiting on escape and there is a range selection, then we leave it in place. However, an -- cgit v1.2.3 From 14f259479bd9e398d477b1d535719407b9c0c618 Mon Sep 17 00:00:00 2001 From: Stephen Blott Date: Fri, 6 Feb 2015 15:34:59 +0000 Subject: Visual/edit modes: code cleanup. - convert getCaretCoordinates from JS to CS - handle x axis in scrollIntoView - better comments throughout. --- content_scripts/mode_visual_edit.coffee | 58 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 27 deletions(-) (limited to 'content_scripts/mode_visual_edit.coffee') diff --git a/content_scripts/mode_visual_edit.coffee b/content_scripts/mode_visual_edit.coffee index ba0bc307..e11c29ec 100644 --- a/content_scripts/mode_visual_edit.coffee +++ b/content_scripts/mode_visual_edit.coffee @@ -26,7 +26,7 @@ # - ..., EditMode, VisualLineMode # -# This prevents printable characters from being passed through to underlying modes or to the underlying page. +# This prevents printable characters from being passed through to underlying modes or the underlying page. class SuppressPrintable extends Mode constructor: (options = {}) -> handler = (event) => @@ -44,7 +44,8 @@ class SuppressPrintable extends Mode class CountPrefix extends SuppressPrintable constructor: (options) -> @countPrefix = "" - # This allows us to implement both "d3w" and "3dw". Also, "3d2w" deletes six words. + # This is an initial multiplier for the first count. It allows edit mode to implement both "d3w" and + # "3dw". Also, "3d2w" deletes six words. @countPrefixFactor = options.initialCountPrefix || 1 super options @@ -61,9 +62,8 @@ class CountPrefix extends SuppressPrintable "" getCountPrefix: -> - count = @countPrefixFactor * if 0 < @countPrefix?.length then parseInt @countPrefix else 1 - @countPrefix = "" - @countPrefixFactor = 1 + count = @countPrefixFactor * (if 0 < @countPrefix.length then parseInt @countPrefix else 1) + @countPrefix = ""; @countPrefixFactor = 1 count # Symbolic names for some common strings. @@ -81,9 +81,11 @@ lineboundary= "lineboundary" class Movement extends CountPrefix opposite: forward: backward, backward: forward + # Paste from clipboard. paste: (callback) -> chrome.runtime.sendMessage handler: "pasteFromClipboard", (response) -> callback response + # Copy to clipboard. 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). @@ -105,7 +107,8 @@ 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. + # Replace the current mode with another. For example, replace caret mode with visual mode, or replace visual + # mode with visual-line mode. changeMode: (mode, options = {}) -> @exit() if @options.parentMode @@ -114,7 +117,7 @@ class Movement extends CountPrefix new mode options # Return the character following (to the right of) the focus, and leave the selection unchanged. Returns - # undefined if there is no such character. + # undefined if no such character exists. getNextForwardCharacter: -> beforeText = @selection.toString() if beforeText.length == 0 or @getDirection() == forward @@ -124,7 +127,7 @@ class Movement extends CountPrefix @selection.modify "extend", backward, character afterText[afterText.length - 1] else - beforeText[0] + beforeText[0] # Existing range selection is backwards. # As above, but backwards. getNextBackwardCharacter: -> @@ -136,9 +139,9 @@ class Movement extends CountPrefix @selection.modify "extend", forward, character afterText[0] else - beforeText[beforeText.length - 1] + beforeText[beforeText.length - 1] # Existing range selection is forwards. - # Test whether the character following the focus is a word character. Leave the selection unchanged. + # Test whether the character following the focus is a word character (and leave the selection unchanged). nextCharacterIsWordCharacter: do -> regexp = /[A-Za-z0-9_]/; -> regexp.test @getNextForwardCharacter() @@ -148,8 +151,8 @@ class Movement extends CountPrefix # @runMovement [ "forward", "word" ] # @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 granularities are word, "character", "line", "lineboundary", "sentence" and "paragraph". In addition, + # we implement the pseudo granularity "vimword", which implements vim-like word movement (for "w"). # runMovement: (args...) -> # Normalize the various argument forms. @@ -217,8 +220,8 @@ class Movement extends CountPrefix which = if direction == forward then "start" else "end" @selection.extend original["#{which}Container"], original["#{which}Offset"] - # Try to extend the selection one character in direction. Return 1, -1 or 0, indicating whether the - # selection got bigger, or smaller, or is unchanged. + # Try to extend the selection one character in direction. Return positive, negative or 0, indicating + # whether the selection got bigger, or smaller, or is unchanged. extendByOneCharacter: (direction) -> length = @selection.toString().length @selection.modify "extend", direction, character @@ -226,8 +229,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). This could be better, see: https://dom.spec.whatwg.org/#interface-range (haowever, that probably - # wouldn't work for text inputs). + # NOTE(smblott). This could be better, see: https://dom.spec.whatwg.org/#interface-range (however, that + # probably wouldn'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 ] @@ -248,8 +251,8 @@ class Movement extends CountPrefix @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). + # A movement can be either a string (which will be passed to @runMovement count times), or a function (which + # will be called once with count as its argument). movements: "l": "forward character" "h": "backward character" @@ -309,7 +312,8 @@ class Movement extends CountPrefix @runMovementKeyChar @options.immediateMovement, @getCountPrefix() return - # This is the main keyboard-event handler for movements and commands. + # This is the main keyboard-event handler for movements and commands for all user modes (visual, + # visual-line, caret and edit). @push _name: "#{@id}/keypress" keypress: (event) => @@ -357,24 +361,24 @@ class Movement extends CountPrefix @movements.n = (count) -> executeFind count, false @movements.N = (count) -> executeFind count, true @movements["/"] = -> - @findMode = enterFindMode() + @findMode = window.enterFindMode() @findMode.onExit => @changeMode VisualMode # # End of Movement constructor. - # Yank the selection; always exits; either deletes the selection or collapses it; set @yankedText and - # returns it. + # Yank the selection; always exits; either deletes the selection or removes it; set @yankedText and return + # it. yank: (args = {}) -> @yankedText = @selection.toString() @selection.deleteFromDocument() if @options.deleteFromDocument or args.deleteFromDocument - @selection.removeAllRanges() + @selection.removeAllRanges() unless @options.parentMode message = @yankedText.replace /\s+/g, " " message = message[...12] + "..." if 15 < @yankedText.length plural = if @yankedText.length == 1 then "" else "s" HUD.showForDuration "Yanked #{@yankedText.length} character#{plural}: \"#{message}\".", 2500 - @options.onYank.call @, @yankedText if @options.onYank + @options.onYank?.call @, @yankedText @exit() @yankedText @@ -434,15 +438,14 @@ class Movement extends CountPrefix char = @getNextForwardCharacter() @runMovement forward, character - # Try to scroll the focus into view. + # Scroll the focus into view. scrollIntoView: -> @protectClipboard => if @element and DomUtils.isEditable @element if @element.clientHeight < @element.scrollHeight if @element.isContentEditable - # WIP... + # WIP (edit mode only)... elementWithFocus = DomUtils.getElementWithFocus @selection, @getDirection() == backward - console.log elementWithFocus.innerHTML # position = @element.getClientRects()[0].top - elementWithFocus.getClientRects()[0].top # console.log "top", position # Scroller.scrollToPosition @element, position, 0 @@ -547,6 +550,7 @@ class VisualMode extends Movement console.log "yank:", @yankedText if @debug @copy @yankedText, true + # Call sub-class; then yank, if we've only been created for a single movement. handleMovementKeyChar: (args...) -> super args... @yank() if @options.oneMovementOnly or @options.immediateMovement -- cgit v1.2.3