From 809eb0d7cf7c33f10feecebbfeac057609ee6bf9 Mon Sep 17 00:00:00 2001 From: Simon Lydell Date: Tue, 19 Apr 2016 19:27:08 +0100 Subject: [PATCH] Add Caret mode Fixes #480. --- documentation/api.md | 4 + documentation/button.md | 3 +- documentation/commands.md | 94 +++++++++++++ documentation/modes.md | 8 +- extension/lib/commands-frame.coffee | 97 ++++++++++++++ extension/lib/commands.coffee | 23 ++++ extension/lib/defaults.coffee | 18 +++ extension/lib/events-frame.coffee | 7 +- extension/lib/events.coffee | 3 + extension/lib/modes.coffee | 86 +++++++++++- extension/lib/selection.coffee | 167 ++++++++++++++++++++++++ extension/lib/utils.coffee | 24 +++- extension/locale/de/vimfx.properties | 19 +++ extension/locale/en-US/vimfx.properties | 19 +++ extension/locale/es/vimfx.properties | 19 +++ extension/locale/fr/vimfx.properties | 19 +++ extension/locale/id/vimfx.properties | 19 +++ extension/locale/it/vimfx.properties | 19 +++ extension/locale/ja/vimfx.properties | 19 +++ extension/locale/nl/vimfx.properties | 19 +++ extension/locale/pt-BR/vimfx.properties | 19 +++ extension/locale/ru/vimfx.properties | 19 +++ extension/locale/sv-SE/vimfx.properties | 19 +++ extension/locale/zh-CN/vimfx.properties | 19 +++ extension/locale/zh-TW/vimfx.properties | 19 +++ 25 files changed, 774 insertions(+), 7 deletions(-) create mode 100644 extension/lib/selection.coffee diff --git a/documentation/api.md b/documentation/api.md index 7329815..ba790bd 100644 --- a/documentation/api.md +++ b/documentation/api.md @@ -777,6 +777,10 @@ The available type strings depend on `id`: - focusable: Any focusable element not falling into another category. - scrollable: Like “scrollable” when `id` is “normal” (see above). +- select: + + - selectable: An element with selectable text (but not text inputs). + The type string can also be `'other'`, regardless of what `id` is. That is the case for elements for markers added by the `` Hints mode command. diff --git a/documentation/button.md b/documentation/button.md index 803e459..54e1594 100644 --- a/documentation/button.md +++ b/documentation/button.md @@ -32,7 +32,8 @@ that matter, some other way.) Other [modes] are easily detectable. You know that you’re in Find mode if the find bar is focused. If there are hint markers visible for the page, you’re in -Hints mode. +Hints mode. Finally, if there’s a blinking caret outside a text input, you’re in +Caret mode. A quick takeaway from this part of the documentation: **If you don’t know what’s going on, look for VimFx’s toolbar button.** It can often help you. diff --git a/documentation/commands.md b/documentation/commands.md index 4feca76..38dd0f9 100644 --- a/documentation/commands.md +++ b/documentation/commands.md @@ -294,6 +294,100 @@ you _really_ want to avoid using the mouse at all costs. [hints\_toggle\_in\_background]: options.md#hints_toggle_in_background +## The `v` commands / Caret mode + +The point of Caret mode is to copy text from web pages using the keyboard. + +### Entering Caret mode + +Pressing `v` will enter Hints mode with hint markers for all elements with text +inside. When activating a marker, its element will get a blinking caret at the +beginning of it, and Caret mode will be entered. + +The `zv` command does the same thing as `v`, but instead of placing the caret at +the beginning of the element, it selects the entire element. + +The `yv` command brings up the same hint markers as `zv` does, and then takes +the text that `zv` would have selected and copies it to the clipboard. It does +not enter Caret mode at all. + +The letter `v` was chosen for these shortcuts because that’s what Vim uses to +enter its Visual mode, which was an inspiration for VimFx’s Caret mode. + +### Caret mode commands + +Caret mode uses [Firefox’s own Caret mode] under the hood. This means that you +can use the arrows keys, ``, ``, `` and `` +(optionally holding ctrl) to move the caret as usual. Hold shift while moving +the caret to select text. + +In addition to the above, VimFx provides a few commands inspired by Vim. + +- `h`, `j`, `k`, `l`: Move the caret left, down, up or right, like the arrow + keys. + +- `b`, `w`: Move the caret one word backward or forward, like `` and + `` but a bit “Vim-adjusted” (see the section on Vim below) in order + to be more useful. + +- `0` (or `^`), `$`: Move the caret to the start or end of the line. + +The above commands (except the ones moving to the start or end of the line) +accept a _count._ For example, press `3w` to move three words forward. + +Press `v` to start selecting text. After doing so, VimFx’s commands for moving +the caret select the text instead of just moving the caret. Press `v` again to +collapse the selection again. (Note that after pressing `v`, only VimFx’s +commands goes into “selection mode,” while Firefox’s work as usual, requiring +shift to be held to select text.) + +`o` moves the caret to the “other end” of the selection. If the caret is at the +end of the selection, `o` will move it to the start (while keeping the selection +intact), and vice versa. This let’s you adjust the selection in both ends. + +Finally, `y` is a possibly faster alternative to the good old ``. Other +than copying the selection to the clipboard, it also exits Caret mode, saving +you yet a keystroke. (`` is unsurprisingly used to exit Caret mode +otherwise.) + +[Firefox’s own Caret mode]: http://kb.mozillazine.org/Accessibility_features_of_Firefox#Allow_text_to_be_selected_with_the_keyboard + +### Workflow tips + +If you’re lucky, the text you want to copy is located within a single element +that contains no other text, such as the text of a link or an inline code +snippet. If so, using the `yv` command (which copies an entire element without +entering Caret mode) is the fastest. + +If you want to copy _almost_ all text of an element, or a bit more than it, use +the `zv` command (which selects an entire element). Then adjust the selection +using the various Caret mode commands. Remember that `o` lets you adjust both +ends of the selection. + +In all other cases, use the `v` command to place the caret close to the text you +want to copy. Then move the caret in place using the various Caret +mode commands, hit `v` to start selecting, and move the again. + +Use `y` to finish (or `` to abort). Alternatively, use the `` key +to open the context menu for the selection. + +### For Vim users + +As seen above, Caret mode is obviously inspired by Vim’s Visual mode. However, +keep in mind that the point of Caret mode is to **copy text using the keyboard, +not mimicing Vim’s visual mode.** I’ve found that selecting text for _copying_ +is different than selecting code for _editing._ Keep that in mind. + +Working with text selection in webpages using code is a terrible mess full of +hacks. New commands will only be added if they _really_ are worth it. + +A note on VimFx’s `b` and `w`: They work like Vim’s `b` and `w` (but a “word” is +according to Firefox’s definition, not Vim’s), except when there is selected +text and the caret is at the end of the selection. Then `b` works like Vim’s +`ge` and `w` works like Vim’s `e`. The idea is to keep it simple and only +provide two commands that do what you want, rather than many just to mimic Vim. + + ## Ignore mode `` Ignore mode is all about ignoring VimFx commands and sending the keys to the diff --git a/documentation/modes.md b/documentation/modes.md index ba1d639..00fecf0 100644 --- a/documentation/modes.md +++ b/documentation/modes.md @@ -33,8 +33,11 @@ These are the modes of VimFx: make sense to [enter Ignore mode by default][blacklist] and stay in Ignore mode most of the time, occasionally popping into Normal mode. -- Hints mode. Entered when using the [`f` commands][f-commands] and let’s click - things by typing the letters of hint markers. +- Hints mode. Entered when using the [`f` commands][f-commands] and lets you + click things by typing the letters of hint markers. + +- Caret mode. Entered when using some of the [`v` commands][v-commands] and lets + you copy text using the keyboard. If you’re unsure which mode you’re in, have a look at VimFx’s toolbar [button]. @@ -69,4 +72,5 @@ Note that the mode is per tab, not global. [autofocus prevention]: options.md#prevent-autofocus [shortcut]: shortcuts.md [f-commands]: commands.md#the-f-commands--hints-mode +[v-commands]: commands.md#the-v-commands--caret-mode [button]: button.md diff --git a/extension/lib/commands-frame.coffee b/extension/lib/commands-frame.coffee index 9d62821..598256f 100644 --- a/extension/lib/commands-frame.coffee +++ b/extension/lib/commands-frame.coffee @@ -23,6 +23,8 @@ # few more generalized “commands” used in more than one place. hints = require('./hints') +prefs = require('./prefs') +SelectionManager = require('./selection') translate = require('./l10n') utils = require('./utils') @@ -286,6 +288,20 @@ commands.follow_focus = helper_follow.bind(null, {id: 'focus', combine: false}, return {type, semantic: true} ) +commands.follow_selectable = helper_follow.bind(null, {id: 'selectable'}, + ({element}) -> + isRelevantTextNode = (node) -> + # Ignore whitespace-only text nodes, and single-letter ones (which are + # common in many syntax highlighters). + return node.nodeType == 3 and node.data.trim().length > 1 + type = + if Array.some(element.childNodes, isRelevantTextNode) + 'selectable' + else + null + return {type, semantic: true} +) + commands.focus_marker_element = ({vim, elementIndex, options}) -> {element} = vim.state.markerElements[elementIndex] # To be able to focus scrollable elements, `FLAG_BYKEY` _has_ to be used. @@ -316,6 +332,46 @@ commands.copy_marker_element = ({vim, elementIndex, property}) -> {element} = vim.state.markerElements[elementIndex] utils.writeToClipboard(element[property]) +commands.element_text_select = ({vim, elementIndex, full}) -> + {element} = vim.state.markerElements[elementIndex] + window = element.ownerGlobal + selection = window.getSelection() + range = window.document.createRange() + + if full + range.selectNodeContents(element) + + # Try to scroll the element into view, but keep the caret visible. + viewport = viewportUtils.getWindowViewport(window) + rect = element.getBoundingClientRect() + block = switch + when rect.bottom > viewport.bottom + 'end' + when rect.top < viewport.top and rect.height < viewport.height + 'start' + else + null + if block + smooth = ( + prefs.root.get('general.smoothScroll') and + prefs.root.get('general.smoothScroll.other') + ) + element.scrollIntoView({ + block + behavior: if smooth then 'smooth' else 'instant' + }) + + else + result = utils.getFirstNonWhitespace(element) + return unless result + [node, offset] = result + range.setStart(node, offset) + range.setEnd(node, offset) + + utils.clearSelectionDeep(vim.content) + window.focus() + selection.addRange(range) + commands.follow_pattern = ({vim, type, options}) -> {document} = vim.content @@ -423,6 +479,7 @@ commands.esc = (args) -> {vim} = args commands.blur_active_element(args) vim.clearHover() + utils.clearSelectionDeep(vim.content) {document} = vim.content if document.exitFullscreen @@ -434,4 +491,44 @@ commands.blur_active_element = ({vim}) -> vim.state.explicitBodyFocus = false utils.blurActiveElement(vim.content) +helper_create_selection_manager = (vim) -> + window = utils.getActiveElement(vim.content)?.ownerGlobal ? vim.content + return new SelectionManager(window) + +commands.enable_caret = ({vim}) -> + return unless selectionManager = helper_create_selection_manager(vim) + selectionManager.enableCaret() + +commands.move_caret = ({vim, method, direction, select, count}) -> + return unless selectionManager = helper_create_selection_manager(vim) + for [0...count] by 1 + if selectionManager[method] + error = selectionManager[method](direction, select) + else + error = selectionManager.moveCaret(method, direction, select) + break if error + return + +commands.toggle_selection = ({vim, select}) -> + return unless selectionManager = helper_create_selection_manager(vim) + if select + vim.notify(translate('notification.toggle_selection.enter')) + else + selectionManager.collapse() + +commands.toggle_selection_direction = ({vim}) -> + return unless selectionManager = helper_create_selection_manager(vim) + selectionManager.reverseDirection() + +commands.get_selection = ({vim}) -> + return unless selectionManager = helper_create_selection_manager(vim) + return selectionManager.selection.toString() + +commands.collapse_selection = ({vim}) -> + return unless selectionManager = helper_create_selection_manager(vim) + selectionManager.collapse() + +commands.clear_selection = ({vim}) -> + utils.clearSelectionDeep(vim.content) + module.exports = commands diff --git a/extension/lib/commands.coffee b/extension/lib/commands.coffee index c88ce72..fad3406 100644 --- a/extension/lib/commands.coffee +++ b/extension/lib/commands.coffee @@ -604,6 +604,29 @@ commands.focus_text_input = ({vim, count}) -> vim.markPageInteraction() vim._run('focus_text_input', {count}) +helper_follow_selectable = ({select}, {vim}) -> + callback = (marker) -> + vim._run('element_text_select', { + elementIndex: marker.wrapper.elementIndex + full: select + }) + vim.enterMode('caret', select) + helper_follow('follow_selectable', vim, callback) + +commands.element_text_caret = + helper_follow_selectable.bind(null, {select: false}) + +commands.element_text_select = + helper_follow_selectable.bind(null, {select: true}) + +commands.element_text_copy = ({vim}) -> + callback = (marker) -> + vim._run('copy_marker_element', { + elementIndex: marker.wrapper.elementIndex + property: 'textContent' + }) + helper_follow('follow_selectable', vim, callback) + findStorage = {lastSearchString: ''} diff --git a/extension/lib/defaults.coffee b/extension/lib/defaults.coffee index 859f96d..633028b 100644 --- a/extension/lib/defaults.coffee +++ b/extension/lib/defaults.coffee @@ -92,6 +92,9 @@ shortcuts = '[': 'follow_previous' ']': 'follow_next' 'gi': 'focus_text_input' + 'v': 'element_text_caret' + 'zv': 'element_text_select' + 'yv': 'element_text_copy' 'find': '/': 'find' @@ -111,6 +114,21 @@ shortcuts = ':': 'dev' '': 'esc' + 'caret': + '': + 'h': 'move_left' + 'l': 'move_right' + 'j': 'move_down' + 'k': 'move_up' + 'b': 'move_word_left' + 'w': 'move_word_right' + '0 ^': 'move_to_line_start' + '$': 'move_to_line_end' + 'v': 'toggle_selection' + 'o': 'toggle_selection_direction' + 'y': 'copy_selection_and_exit' + '': 'exit' + 'hints': '': '': 'exit' diff --git a/extension/lib/events-frame.coffee b/extension/lib/events-frame.coffee index fad5d15..87bd952 100644 --- a/extension/lib/events-frame.coffee +++ b/extension/lib/events-frame.coffee @@ -207,12 +207,15 @@ class FrameEventManager @vim.state.hasInteraction = false return - # Save the last focused text input regardless of whether that input might - # be blurred because of autofocus prevention. if utils.isTextInputElement(target) + # Save the last focused text input regardless of whether that input + # might be blurred because of autofocus prevention. @vim.state.lastFocusedTextInput = target @vim.state.hasFocusedTextInput = true + if @vim.mode == 'caret' and not utils.isContentEditable(target) + @vim.enterMode('normal') + # Blur the focus target, if autofocus prevention is enabled… if prefs.get('prevent_autofocus') and @vim.mode in prefs.get('prevent_autofocus_modes').split(/\s+/) and diff --git a/extension/lib/events.coffee b/extension/lib/events.coffee index 14b9add..066f16b 100644 --- a/extension/lib/events.coffee +++ b/extension/lib/events.coffee @@ -106,6 +106,9 @@ class UIEventManager focusType = utils.getFocusType(utils.getActiveElement(@window)) vim._setFocusType(focusType) + if focusType == 'editable' and vim.mode == 'caret' + vim.enterMode('normal') + @listen('focus', handleFocusRelatedEvent) @listen('blur', handleFocusRelatedEvent) diff --git a/extension/lib/modes.coffee b/extension/lib/modes.coffee index 5f8e809..9046dcd 100644 --- a/extension/lib/modes.coffee +++ b/extension/lib/modes.coffee @@ -28,8 +28,13 @@ help = require('./help') hints = require('./hints') translate = require('./l10n') {rotateOverlappingMarkers} = require('./marker') +prefs = require('./prefs') +SelectionManager = require('./selection') utils = require('./utils') +{FORWARD, BACKWARD} = SelectionManager +CARET_BROWSING_PREF = 'accessibility.browsewithcaret' + # Helper to create modes in a DRY way. mode = (modeName, obj, commands = null) -> obj.name = translate("mode.#{modeName}") @@ -112,6 +117,83 @@ mode('normal', { +helper_move_caret = (method, direction, {vim, storage, count = 1}) -> + vim._run('move_caret', { + method, direction, select: storage.select + count: if method == 'intraLineMove' then 1 else count + }) + +mode('caret', { + onEnter: ({vim, storage}, select = false) -> + storage.select = select + storage.caretBrowsingPref = prefs.root.get(CARET_BROWSING_PREF) + prefs.root.set(CARET_BROWSING_PREF, true) + vim._run('enable_caret') + + listener = -> + return unless newVim = vim._parent.getCurrentVim(vim.window) + prefs.root.set( + CARET_BROWSING_PREF, + if newVim.mode == 'caret' then true else storage.caretBrowsingPref + ) + vim._parent.on('TabSelect', listener) + storage.removeListener = -> vim._parent.off('TabSelect', listener) + + onLeave: ({vim, storage}) -> + prefs.root.set(CARET_BROWSING_PREF, storage.caretBrowsingPref) + vim._run('clear_selection') + storage.removeListener?() + storage.removeListener = null + + onInput: (args, match) -> + {vim, storage} = args + if match.type == 'full' + match.command.run(args) + return true + return false + +}, { + # coffeelint: disable=colon_assignment_spacing + move_left: helper_move_caret.bind(null, 'characterMove', BACKWARD) + move_right: helper_move_caret.bind(null, 'characterMove', FORWARD) + move_down: helper_move_caret.bind(null, 'lineMove', FORWARD) + move_up: helper_move_caret.bind(null, 'lineMove', BACKWARD) + move_word_left: helper_move_caret.bind(null, 'wordMoveAdjusted', BACKWARD) + move_word_right: helper_move_caret.bind(null, 'wordMoveAdjusted', FORWARD) + move_to_line_start: helper_move_caret.bind(null, 'intraLineMove', BACKWARD) + move_to_line_end: helper_move_caret.bind(null, 'intraLineMove', FORWARD) + # coffeelint: enable=colon_assignment_spacing + + toggle_selection: ({vim, storage}) -> + storage.select = not storage.select + if storage.select + vim.notify(translate('notification.toggle_selection.enter')) + else + vim._run('collapse_selection') + + toggle_selection_direction: ({vim}) -> + vim._run('toggle_selection_direction') + + copy_selection_and_exit: ({vim}) -> + vim._run('get_selection', null, (selection) -> + # If the selection consists of newlines only, it _looks_ as if the + # selection is collapsed, so don’t try to copy it in that case. + if /^\n*$/.test(selection) + vim.notify(translate('notification.copy_selection_and_exit.none')) + else + # Trigger this copying command instead of putting `selection` into the + # clipboard, since `window.getSelection().toString()` sadly collapses + # whitespace in `
` elements.
+        vim.window.goDoCommand('cmd_copy')
+        vim.enterMode('normal')
+    )
+
+  exit: ({vim}) ->
+    vim.enterMode('normal')
+})
+
+
+
 mode('hints', {
   onEnter: ({vim, storage}, markers, callback, count = 1, sleep = -1) ->
     storage.markers = markers
@@ -176,7 +258,9 @@ mode('hints', {
           marker.reset() for marker in markers
           storage.numEnteredChars = 0
         else
-          vim.enterMode('normal')
+          # The callback might have entered another mode. Only go back to Normal
+          # mode if we’re still in Hints mode.
+          vim.enterMode('normal') if vim.mode == 'hints'
       else
         storage.numEnteredChars += 1
 
diff --git a/extension/lib/selection.coffee b/extension/lib/selection.coffee
new file mode 100644
index 0000000..d168137
--- /dev/null
+++ b/extension/lib/selection.coffee
@@ -0,0 +1,167 @@
+###
+# Copyright Simon Lydell 2016.
+#
+# This file is part of VimFx.
+#
+# VimFx is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# VimFx is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with VimFx.  If not, see .
+###
+
+# This file helps dealing with text selection: Querying it, modifying it and
+# moving its caret.
+
+FORWARD = true
+BACKWARD = false
+
+class SelectionManager
+  constructor: (@window) ->
+    @selection = @window.getSelection()
+    @nsISelectionController = @window
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsIWebNavigation)
+      .QueryInterface(Ci.nsIInterfaceRequestor)
+      .getInterface(Ci.nsISelectionDisplay)
+      .QueryInterface(Ci.nsISelectionController)
+
+  @FORWARD = FORWARD
+  @BACKWARD = BACKWARD
+
+  enableCaret: ->
+    @nsISelectionController.setCaretEnabled(true)
+    @nsISelectionController.setCaretReadOnly(false)
+    @nsISelectionController.setCaretVisibilityDuringSelection(true)
+
+  collapse: ->
+    return if @selection.isCollapsed
+    direction = @getDirection()
+    if direction == FORWARD
+      @selection.collapseToEnd()
+    else
+      @selection.collapseToStart()
+
+  moveCaretThrowing: (method, direction, select = true) ->
+    @nsISelectionController[method](direction, select)
+
+  moveCaret: (args...) ->
+    try
+      @moveCaretThrowing(args...)
+    catch error
+      return error
+    return null
+
+  # The simplest way to measure selection length is
+  # `selection.toString().length`. However, `selection.toString()` collapses
+  # whitespace even in `
` elements, so it is sadly not reliable. Instead we
+  # have to measure client rects.
+  getSelectionLength: ->
+    width = 0
+    numRects = 0
+    for index in [0...@selection.rangeCount] by 1
+      for rect in @selection.getRangeAt(index).getClientRects()
+        width += rect.width
+        numRects += 1
+    return [width, numRects]
+
+  getDirection: (directionIfCollapsed = FORWARD) ->
+    # The “test for newlines” trick used in `@reverseDirection` should _not_ be
+    # used here. If it were, selecting the newline(s) between two paragraphs and
+    # then `@collapse()`ing that selection might move the caret.
+    return directionIfCollapsed if @selection.isCollapsed
+
+    # Creating backwards ranges is not supported. When trying to do so,
+    # `range.toString()` returns the empty string.
+    range = @window.document.createRange()
+    range.setStart(@selection.anchorNode, @selection.anchorOffset)
+    range.setEnd(@selection.focusNode, @selection.focusOffset)
+    return range.toString() != ''
+
+  reverseDirection: ->
+    # If the caret is at the end of a paragraph, or at the start of the
+    # paragraph, and the newline(s) between those paragraphs happen to be
+    # selected, it _looks_ as if `selection.isCollapsed` should be `true`, but
+    # it isn't because of said (virtual) newline characters. If so, the below
+    # algorithm might move the caret from the start of a paragraph to the end of
+    # the previous paragraph, etc. So don’t do anything if the selection is
+    # empty or newlines only.
+    return if /^\n*$/.test(@selection.toString())
+
+    direction = @getDirection()
+
+    range = @selection.getRangeAt(0)
+    edge = if direction == FORWARD then 'start' else 'end'
+    {"#{edge}Container": edgeElement, "#{edge}Offset": edgeOffset} = range
+    range.collapse(not direction)
+    @selection.removeAllRanges()
+    @selection.addRange(range)
+    @selection.extend(edgeElement, edgeOffset)
+
+    # When going from backward to forward the caret might end up at the line
+    # _after_ the selection if the selection ends at the end of a line, which
+    # looks a bit odd. This adjusts that case.
+    if direction == BACKWARD
+      [oldWidth] = @getSelectionLength()
+      @moveCaret('characterMove', BACKWARD)
+      [newWidth] = @getSelectionLength()
+      unless newWidth == oldWidth
+        @moveCaret('characterMove', FORWARD)
+
+  wordMoveAdjusted: (direction, select = true) ->
+    selectionDirection = @getDirection(direction)
+
+    try
+      if (select and selectionDirection != direction) or
+         (not select and direction == FORWARD)
+        @_wordMoveAdjusted(direction)
+      else
+        @moveCaretThrowing('wordMove', direction, select)
+    catch error
+      throw error unless error.name == 'NS_ERROR_FAILURE'
+      @collapse() unless select
+      return error
+
+    unless select
+      # When at the very end of the document `@_wordMoveAdjusted(FORWARD)` might
+      # end up moving the caret _backward!_ If so, move the caret back.
+      @moveCaret('wordMove', direction) if @getDirection(direction) != direction
+      @collapse()
+
+    return null
+
+  _wordMoveAdjusted: (direction) ->
+    [oldWidth, oldNumRects] = @getSelectionLength()
+
+    # Do the old “two steps forward and one step back” trick to avoid the
+    # selection ending with whitespace. (Vice versa for backwards selections.)
+    @moveCaretThrowing('wordMove', direction)
+    @moveCaretThrowing('wordMove', direction)
+    @moveCaretThrowing('wordMove', not direction)
+
+    [newWidth, newNumRects] = @getSelectionLength()
+
+    # However, in some cases the above can result in the caret not moving at
+    # all. If so, go _three_ steps forward and one back. (Again, vice versa for
+    # backwards selections.)
+    if oldNumRects == newNumRects and oldWidth == newWidth
+      @moveCaretThrowing('wordMove', direction)
+      @moveCaretThrowing('wordMove', direction)
+      @moveCaretThrowing('wordMove', direction)
+      @moveCaretThrowing('wordMove', not direction)
+
+    [newWidth, newNumRects] = @getSelectionLength()
+
+    # Finally, if everything else failed to move the caret (such as when being
+    # one word from the end of the document), simply move one step.
+    if oldNumRects == newNumRects and oldWidth == newWidth
+      @moveCaretThrowing('wordMove', direction)
+
+module.exports = SelectionManager
diff --git a/extension/lib/utils.coffee b/extension/lib/utils.coffee
index 5c955b6..6db3a8d 100644
--- a/extension/lib/utils.coffee
+++ b/extension/lib/utils.coffee
@@ -318,6 +318,16 @@ simulateMouseEvents = (element, sequence) ->
 area = (element) ->
   return element.clientWidth * element.clientHeight
 
+clearSelectionDeep = (window) ->
+  # The selection might be `null` in hidden frames.
+  selection = window.getSelection()
+  selection?.removeAllRanges()
+  for frame in window.frames
+    clearSelectionDeep(frame)
+    # Allow parents to re-gain control of text selection.
+    frame.frameElement.blur()
+  return
+
 containsDeep = (parent, element) ->
   parentWindow = parent.ownerGlobal
   elementWindow = element.ownerGlobal
@@ -337,6 +347,16 @@ createBox = (document, className = '', parent = null, text = null) ->
   parent.appendChild(box) if parent?
   return box
 
+getFirstNonWhitespace = (element) ->
+  for node in element.childNodes then switch node.nodeType
+    when 3 # TextNode.
+      offset = node.data.search(/\S/)
+      return [node, offset] if offset >= 0
+    when 1 # Element
+      result = getFirstNonWhitespace(node)
+      return result if result
+  return null
+
 getFrameViewport = (frame, parentViewport) ->
   rect = frame.getBoundingClientRect()
   return null unless isInsideViewport(rect, parentViewport)
@@ -644,10 +664,12 @@ module.exports = {
   simulateMouseEvents
 
   area
+  clearSelectionDeep
   containsDeep
   createBox
-  getRootElement
+  getFirstNonWhitespace
   getFrameViewport
+  getRootElement
   getViewportCappedClientHeight
   getWindowViewport
   injectTemporaryPopup
diff --git a/extension/locale/de/vimfx.properties b/extension/locale/de/vimfx.properties
index 4e031f7..0d53fcc 100644
--- a/extension/locale/de/vimfx.properties
+++ b/extension/locale/de/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Browserelement anklicken
 mode.normal.follow_previous="Vorherigen" Link anklicken
 mode.normal.follow_next="Nächsten" Link ankliken
 mode.normal.focus_text_input=Zuletzt fokussierte oder erste Texteingabe fokussieren
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Suchen
 mode.normal.find=Suchmodus aktivieren
@@ -93,6 +96,20 @@ mode.normal.help=Hilfedialog anzeigen
 mode.normal.dev=Entwickler-Werkzeuge öffnen
 mode.normal.esc=Aktives Element schließen/verlassen
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Hinweismodus
 mode.hints.exit=Zum Normalmodus zurückkehren
 mode.hints.rotate_markers_forward=Überlappende Markierungen vorwärts drehen
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Lesemodus nicht verfügbar
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Zeichen für Hinweise
 pref.hint_chars.desc=
diff --git a/extension/locale/en-US/vimfx.properties b/extension/locale/en-US/vimfx.properties
index 1f7518e..03eeb17 100644
--- a/extension/locale/en-US/vimfx.properties
+++ b/extension/locale/en-US/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Focus last focused or first text input
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Find
 mode.normal.find=Enter Find mode
@@ -93,6 +96,20 @@ mode.normal.help=Show the Keyboard Shortcuts help dialog
 mode.normal.dev=Open the Developer Toolbar
 mode.normal.esc=Blur/close active element
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Hints mode
 mode.hints.exit=Return to Normal mode
 mode.hints.rotate_markers_forward=Rotate overlapping markers forward
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Hint chars
 pref.hint_chars.desc=
diff --git a/extension/locale/es/vimfx.properties b/extension/locale/es/vimfx.properties
index 907354f..aa22fbd 100644
--- a/extension/locale/es/vimfx.properties
+++ b/extension/locale/es/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Pulsar elemento del navegador
 mode.normal.follow_previous=Pulsar enlace tipo "Anterior"
 mode.normal.follow_next=Pulsar enlace tipo "Siguiente"
 mode.normal.focus_text_input=Poner foco en la última entrada de texto con foco o la primera
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Búsqueda
 mode.normal.find=Entrar en modo Buscar
@@ -93,6 +96,20 @@ mode.normal.help=Mostrar el cuadro de diálogo de ayuda de Atajos de Teclado
 mode.normal.dev=Abrir la Barra de Herramientas de Desarrollador
 mode.normal.esc=Atenuar/Cerrar elemento activo
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Modo Indicaciones
 mode.hints.exit=Volver al modo Normal
 mode.hints.rotate_markers_forward=Rotar indicaciones superpuestas hacia delante
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Vista de lectura no disponible
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Caracteres de indicación
 pref.hint_chars.desc=
diff --git a/extension/locale/fr/vimfx.properties b/extension/locale/fr/vimfx.properties
index 38bbb65..9b60826 100644
--- a/extension/locale/fr/vimfx.properties
+++ b/extension/locale/fr/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Focaliser la dernière entrée de texte à avoir été focalisé ou la première
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Recherche
 mode.normal.find=Rechercher dans la page
@@ -93,6 +96,20 @@ mode.normal.help=Afficher cette page d'aide
 mode.normal.dev=Ouvrir la barre d'outils de développement
 mode.normal.esc=Revenir en mode normal (supprimer le marqueurs, sortir du mode Ignorer) ou quitter l'élément actif
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Mode de sélection de lien
 mode.hints.exit=Retourner au mode initial
 mode.hints.rotate_markers_forward=Faire tourner vers l'avant les marqueurs superposés
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Marqueurs de liens
 pref.hint_chars.desc=
diff --git a/extension/locale/id/vimfx.properties b/extension/locale/id/vimfx.properties
index 3d400ad..f30c969 100644
--- a/extension/locale/id/vimfx.properties
+++ b/extension/locale/id/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Fokus pada fokus terakhir atau masukan teks pertama
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Cari
 mode.normal.find=Masuk mode Cari
@@ -93,6 +96,20 @@ mode.normal.help=Tampilkan dialog bantuan
 mode.normal.dev=Buka Developer Toolbar
 mode.normal.esc=Blur/tutup element aktif
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Mode Petunjuk
 mode.hints.exit=Kembali ke mode Normal
 mode.hints.rotate_markers_forward=Putar maju penanda bertumpukan
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Karakter Petunjuk
 pref.hint_chars.desc=
diff --git a/extension/locale/it/vimfx.properties b/extension/locale/it/vimfx.properties
index 7aec80b..ebc022b 100644
--- a/extension/locale/it/vimfx.properties
+++ b/extension/locale/it/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Seleziona l'ultimo input selezionato o il primo
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Trova
 mode.normal.find=Entra nella modalità ricerca
@@ -93,6 +96,20 @@ mode.normal.help=Mostra la finestra di aiuto
 mode.normal.dev=Apri la barra degli strumenti Sviluppatore
 mode.normal.esc=Chiude l'elemento attivo
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Modalità suggerimenti
 mode.hints.exit=Ritorna alla modalità normale
 mode.hints.rotate_markers_forward=Ruota i marcatori sovrappossti in avanti
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Caratteri di suggerimento
 pref.hint_chars.desc=
diff --git a/extension/locale/ja/vimfx.properties b/extension/locale/ja/vimfx.properties
index 66ffdb6..556903a 100644
--- a/extension/locale/ja/vimfx.properties
+++ b/extension/locale/ja/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=ブラウザ要素をクリック
 mode.normal.follow_previous=「前へ」リンクをクリック
 mode.normal.follow_next=「次へ」リンクをクリック
 mode.normal.focus_text_input=最後にフォーカスしたテキスト入力または最初のテキスト入力にフォーカス
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=検索
 mode.normal.find=ページ内検索モードに入る
@@ -93,6 +96,20 @@ mode.normal.help=ヘルプダイアログを表示
 mode.normal.dev=開発者ツールバーを開く
 mode.normal.esc=アクティブな要素のフォーカスをはずす/要素を閉じる
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=ヒントモード
 mode.hints.exit=ノーマルモードへ戻る
 mode.hints.rotate_markers_forward=重なったマーカーを前へ入れ替え
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=リーダービューは使用できませ
 notification.reload_config_file.none=設定ファイルがみつかりません
 notification.reload_config_file.success=設定ファイルの再読込みに成功しました
 notification.reload_config_file.failure=設定ファイルの再読込みに失敗しました。ブラウザのコンソールを確認してください
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=ヒント機能で使用する文字
 pref.hint_chars.desc=
diff --git a/extension/locale/nl/vimfx.properties b/extension/locale/nl/vimfx.properties
index 62a70b1..3997dea 100644
--- a/extension/locale/nl/vimfx.properties
+++ b/extension/locale/nl/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Focus laatst gefocuste or eerste invoerveld
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Zoeken
 mode.normal.find=Zoeken
@@ -93,6 +96,20 @@ mode.normal.help=Toon de gebruiksaanwijzing
 mode.normal.dev=Open de ontwikkelaarswerkbalk
 mode.normal.esc=Vervaag/sluit huidig element
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Navigeermodus
 mode.hints.exit=Ga terug naar normale modus
 mode.hints.rotate_markers_forward=Ga vooruit in markervolgorde
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Gebruikte karakters
 pref.hint_chars.desc=
diff --git a/extension/locale/pt-BR/vimfx.properties b/extension/locale/pt-BR/vimfx.properties
index a238873..29e5c92 100644
--- a/extension/locale/pt-BR/vimfx.properties
+++ b/extension/locale/pt-BR/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Focar no último focado ou primeiro campo texto
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Localizar
 mode.normal.find=Entrar no modo de Busca
@@ -93,6 +96,20 @@ mode.normal.help=Mostrar o diálogo de ajuda
 mode.normal.dev=Abrir a Barra de Ferramentas de Desenvolvedor
 mode.normal.esc=Desfocar/fechar elemento ativo
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Modo Sugestões
 mode.hints.exit=Voltar ao modo Normal
 mode.hints.rotate_markers_forward=Rotacionar marcadores sobrepostos para frente
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Caracteres de Sugestão
 pref.hint_chars.desc=
diff --git a/extension/locale/ru/vimfx.properties b/extension/locale/ru/vimfx.properties
index 32b42a5..3249eb3 100644
--- a/extension/locale/ru/vimfx.properties
+++ b/extension/locale/ru/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=Вернуть фокус на поле вводa
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=Поиск
 mode.normal.find=Режим поиска
@@ -93,6 +96,20 @@ mode.normal.help=Показать справку
 mode.normal.dev=Открыть Панель Разработки
 mode.normal.esc=Закрыть/покинуть элемент с фокусом
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Режим маркеров
 mode.hints.exit=Вернуться в нормальный режим
 mode.hints.rotate_markers_forward=Переставить перекрывающиеся маркеры
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=Символы на маркерах
 pref.hint_chars.desc=
diff --git a/extension/locale/sv-SE/vimfx.properties b/extension/locale/sv-SE/vimfx.properties
index 0f8e0ab..c32169e 100644
--- a/extension/locale/sv-SE/vimfx.properties
+++ b/extension/locale/sv-SE/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Klicka på webbläsarelement
 mode.normal.follow_previous=Klicka på ”Föregående”-länk
 mode.normal.follow_next=Klicka på ”Nästa”-länk
 mode.normal.focus_text_input=Fokusera senast fokuserade eller den första textrutan
+mode.normal.element_text_caret=Placera markör vid element
+mode.normal.element_text_select=Markera element-text
+mode.normal.element_text_copy=Kopiera element-text
 
 category.find=Sök
 mode.normal.find=Gå till Sökläge
@@ -93,6 +96,20 @@ mode.normal.help=Visa hjälp-rutan för tangentbordskommandon
 mode.normal.dev=Öppna utvecklarverktygsfältet
 mode.normal.esc=Avfokusera/stäng aktivt element
 
+mode.caret=Markörläge
+mode.caret.move_left=Vänster
+mode.caret.move_right=Höger
+mode.caret.move_down=Ned
+mode.caret.move_up=Upp
+mode.caret.move_word_left=Ord vänster
+mode.caret.move_word_right=Ord höger
+mode.caret.move_to_line_start=Början av raden
+mode.caret.move_to_line_end=Slutet av raden
+mode.caret.toggle_selection=Växla mellan markering och ingen markering
+mode.caret.toggle_selection_direction=Gå till andra änden av markeringen
+mode.caret.copy_selection_and_exit=Kopiera markeringen och återvänd till Normalläge
+mode.caret.exit=Återvänd till Normalläge
+
 mode.hints=Etikettsläge
 mode.hints.exit=Återvänd till Normalläge
 mode.hints.rotate_markers_forward=Rotera överlappande etiketter framåt
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Läsarvy inte tillgänglig
 notification.reload_config_file.none=Ingen config-fil hittades
 notification.reload_config_file.success=Config-fil framgångsrikt omladdad
 notification.reload_config_file.failure=Fel vid omladdning av config-fil; kolla webbläsar-konsolen
+notification.toggle_selection.enter=Förflyttning av markören markerar nu text
+notification.copy_selection_and_exit.none=Ingen markering att kopiera
 
 pref.hint_chars.title=Etikettstecken
 pref.hint_chars.desc=
diff --git a/extension/locale/zh-CN/vimfx.properties b/extension/locale/zh-CN/vimfx.properties
index e8fd61e..558a4af 100644
--- a/extension/locale/zh-CN/vimfx.properties
+++ b/extension/locale/zh-CN/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=点击浏览器元素
 mode.normal.follow_previous=点击“上一页”链接
 mode.normal.follow_next=点击“下一页”链接
 mode.normal.focus_text_input=聚焦最后聚焦过的或第一个输入框
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=查找
 mode.normal.find=进入查找模式
@@ -93,6 +96,20 @@ mode.normal.help=显示帮助框
 mode.normal.dev=打开开发者工具栏
 mode.normal.esc=取消聚焦/关闭激活的元素
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Hint 模式
 mode.hints.exit=返回普通模式
 mode.hints.rotate_markers_forward=向前旋转重叠的标记
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=阅读模式不可用
 notification.reload_config_file.none=未发现配置文件
 notification.reload_config_file.success=重新载入配置文件成功
 notification.reload_config_file.failure=重新载入配置文件出错; 检查浏览器控制台
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=提示符
 pref.hint_chars.desc=
diff --git a/extension/locale/zh-TW/vimfx.properties b/extension/locale/zh-TW/vimfx.properties
index abbecdc..a60b430 100644
--- a/extension/locale/zh-TW/vimfx.properties
+++ b/extension/locale/zh-TW/vimfx.properties
@@ -74,6 +74,9 @@ mode.normal.click_browser_element=Click browser element
 mode.normal.follow_previous=Click “Previous” link
 mode.normal.follow_next=Click “Next” link
 mode.normal.focus_text_input=移至最後聚焦過或第一個輸入欄位
+mode.normal.element_text_caret=Place caret at element
+mode.normal.element_text_select=Select element text
+mode.normal.element_text_copy=Copy element text
 
 category.find=搜尋
 mode.normal.find=進入搜尋模式
@@ -93,6 +96,20 @@ mode.normal.help=顯示提示對話框
 mode.normal.dev=開啟開發者工具列
 mode.normal.esc=取消焦點/關閉啟用的元素
 
+mode.caret=Caret mode
+mode.caret.move_left=Left
+mode.caret.move_right=Right
+mode.caret.move_down=Down
+mode.caret.move_up=Up
+mode.caret.move_word_left=Word left
+mode.caret.move_word_right=Word right
+mode.caret.move_to_line_start=Start of line
+mode.caret.move_to_line_end=End of line
+mode.caret.toggle_selection=Toggle selection
+mode.caret.toggle_selection_direction=Go to the other end of the selection
+mode.caret.copy_selection_and_exit=Copy selection and return to Normal mode
+mode.caret.exit=Return to Normal mode
+
 mode.hints=Hints 模式
 mode.hints.exit=回到正常模式
 mode.hints.rotate_markers_forward=向前旋轉重疊的標誌
@@ -132,6 +149,8 @@ notification.enter_reader_view.none=Reader View not available
 notification.reload_config_file.none=No config file found
 notification.reload_config_file.success=Config file successfully reloaded
 notification.reload_config_file.failure=Error reloading config file; check the browser console
+notification.toggle_selection.enter=Moving the caret now selects text
+notification.copy_selection_and_exit.none=No text selection to copy
 
 pref.hint_chars.title=提示字元(Hint Chars)
 pref.hint_chars.desc=進入 Hints 模式時,用哪些符號進行標記。
-- 
2.39.3