aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--content_scripts/mode.coffee1
-rw-r--r--content_scripts/mode_visual_edit.coffee128
2 files changed, 66 insertions, 63 deletions
diff --git a/content_scripts/mode.coffee b/content_scripts/mode.coffee
index c2e70e91..e7a4e0ee 100644
--- a/content_scripts/mode.coffee
+++ b/content_scripts/mode.coffee
@@ -91,7 +91,6 @@ class Mode
# be unique. New instances deactivate existing instances with the same key.
if @options.singleton
do =>
- console.log @options.singleton
@deactivateSingleton @options.singleton
@onExit => Mode.singletons = Mode.singletons.filter (active) => active.key != @options.singleton
Mode.singletons.push key: @options.singleton, mode: @
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 =>