diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelper.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelper.kt
index ac0fe26c6..a6cfdf398 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelper.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelper.kt
@@ -140,9 +140,6 @@ interface VimSearchHelper {
    * @param bigWord    Use WORD instead of word boundaries
    * @param stopOnEmptyLine Vim considers an empty line to be a word/WORD, but `e` and `E` don't respect this for vi
    *                        compatibility reasons. Callers other than `e` and `E` should pass `true`
-   * @param allowMoveFromWordEnd If we're already at the word end, should we be able to move to the next word end? This
-   *                             is true for word/WORD motions `e`/`E`, but false for word text objects, which do not
-   *                             extend the selection/range forwards when at the end of a current word.
    * @return The offset of the [count] next word/WORD. Will return document bounds if not found
    */
   fun findNextWordEnd(
@@ -151,7 +148,6 @@ interface VimSearchHelper {
     count: Int,
     bigWord: Boolean,
     stopOnEmptyLine: Boolean = true,
-    allowMoveFromWordEnd: Boolean = true,
   ): Int
 
   /**
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelperBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelperBase.kt
index 3c151b2ea..3eda0c25c 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelperBase.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimSearchHelperBase.kt
@@ -128,13 +128,12 @@ abstract class VimSearchHelperBase : VimSearchHelper {
     count: Int,
     bigWord: Boolean,
     stopOnEmptyLine: Boolean,
-    allowMoveFromWordEnd: Boolean
   ): Int {
     val text = editor.text()
     var pos = searchFrom
     repeat(abs(count)) {
       pos = if (count > 0) {
-        findNextWordEndOne(text, editor, pos, bigWord, stopOnEmptyLine, allowMoveFromWordEnd)
+        findNextWordEndOne(text, editor, pos, bigWord, stopOnEmptyLine, allowMoveFromWordEnd = true)
       } else {
         findPreviousWordEndOne(text, editor, pos, bigWord)
       }
@@ -1544,32 +1543,18 @@ abstract class VimSearchHelperBase : VimSearchHelper {
       var count = count
       var shouldEndOnWhitespace = false
 
-      // Selecting word/WORDs (forwards):
-      // If there's no selection, we need to calculate the first range:
-      // -> Move back to the first character _on the current line_ of the current character type
-      //    If we're on whitespace, this is the start of the preceding whitespace
-      //    If we're on a word/WORD char, it's the start of the word/WORD
-      // -> Move forward to the end of the next word/WORD or whitespace block
-      //    (Remember that `${se}` in the following examples is at `end+1`)
-      //    For outer objects and currently on whitespace, move to end of the next word/WORD         ("${s}     word${se}")
-      //      New lines are treated as whitespace, so this will wrap and move to the end of the next word/WORD.
-      //      Empty lines will be treated as a word.
-      //    For outer objects on a word/WORD char, move to one character _before_ the next word/WORD ("${s}word ${se}word")
-      //      New lines should be treated as a stop character, and we stop before the new line.
-      //    For inner objects on a word/WORD char, move to the end of this word/WORD                 ("${s}word${se} word")
-      //      This will never encounter a new line character.
-      //    For inner objects on whitespace, move to one character _before_ the next word/WORD       ("${s}     ${se}word")
-      //      New lines should be treated as a stop character, and we stop before the new line.
-      // -> Subtract 1 from count
-      // Once we have a range, or if there's an initial selection:
-      // -> Loop over count
-      //   -> For inner objects, move to the end of the next character type block. Whitespace counts in the loop
-      //   -> For outer objects, move to the character _before_ the next word/WORD. Therefore, whitespace does not count
-      // For all of these operations, remember that an empty line is a word.
+      // Note: for more detailed comments with examples, check git history!
 
+      end = pos
+
+      // If there's no selection, calculate the initial range by moving back to the start of the current character type
+      // on the current line (word/WORD or whitespace). Then move forward:
+      // * For inner objects, move to the end of the current word or whitespace block (or line).
+      // * For outer objects, whitespace is included. Move to the end of the current word (or line) and following
+      //   whitespace (if any), or move to the end of the current whitespace (possibly wrapping) and following word.
+      // Note that the flag for selection is only true if the selection is greater than a single char. Also remember
+      // that an empty line is a word and there are multiple word types not necessarily separated by whitespace.
       if (!hasSelection) {
-        // Move back to the first character of the current character type on the current line.
-        // This will be the start of the word/WORD or the start of whitespace.
         val startingCharacterType = charType(editor, chars[pos], isBig)
         start = pos
         if (!isEmptyLine(chars, start)) {
@@ -1579,192 +1564,94 @@ abstract class VimSearchHelperBase : VimSearchHelper {
           start++
         }
 
-        // Move forward, including or skipping whitespace as necessary. Move from the start of the current
-        // word/whitespace rather than the original position, so that it's easier to handle moving to the end of a word
-        // when the original position is already at the end of the word.
-        // Note that `onSpace` is the character type of the original position, but this is also the character type of
-        // the current start position
-        end = when {
-          // We're on preceding whitespace. Include it, and move to the end of the next word/WORD. Newlines are
-          // considered whitespace and this can wrap to the next line. An empty line will be considered a word and
-          // included.
-          isOuter && onSpace ->             // "${s}     word${se}"
-            findNextWordEnd(editor, start, 1, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
+        end = if ((!isOuter && isWhitespace(editor, chars[start], isBig))
+          || (isOuter && !isWhitespace(editor, chars[start], isBig))) {
 
-          // We're on a word, move to the end, and include following whitespace by moving to the character before the
-          // next word. Newlines are not considered part of whitespace, not included, and this does not wrap.
-          isOuter && !onSpace -> {          // "${s}word ${se}word"
+          // * Inner object, on whitespace. Skip forward to the end of the current whitespace, just before the next
+          //   word or end of line (no wrapping). This will always move us forward one character, so it's always safe to
+          //   move one character back. If we're moving on to an empty line (newline is whitespace!) this will move one
+          //   character forward and then one character back. I.e. `viw` on an empty line only selects the line!
+          // * Outer object, on word. Skip the current word and include any following whitespace. We know this isn't an
+          //   empty line and that we'll stop at the end of the current line, so it's always safe to move back on char.
+          if (isOuter) {
+            // Outer objects should include following whitespace. But if there isn't any, we should walk back and
+            // include any preceding whitespace.
             shouldEndOnWhitespace = true
-
-            // Outer object should include following whitespace. Skip forward over the current word and following
-            // whitespace. We know this isn't an empty line, and that we'll stop at the end of line, so it's always safe
-            // to move back one character.
-            val offset = findNextWordOne(chars, editor, start, isBig, stopAtEndOfLine = true)
-            skipOneCharacterBack(offset)
           }
 
-          // We're on a word, move to the end, not including trailing whitespace. This never includes whitespace, and so
-          // never wraps
-          !isOuter && !onSpace ->           // "${s}word${se} word"
-            findNextWordEnd(editor, start, 1, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
-
-          // We're on preceding whitespace, move to the character before the next word. Newlines are not considered
-          // whitespace and this does not wrap. Empty lines also do not wrap.
-          else /* !isOuter && onSpace */ -> { // "${s}     ${se}word"
-
-            // Inner object does not include whitespace, but does count it. Skip forward over the current whitespace
-            // until we find a new word or the end of line. The implementation of `findNextWordOne` will always move at
-            // least one character forward, so it's always safe to move one character back. If we are on an empty line,
-            // `findNextWordOne` will still move one character forward, taking us to the next line. Moving one back will
-            // return us to the original offset. You can see this with `viw` on an empty line - it only selects the
-            // current line.
-            val offset = findNextWordOne(chars, editor, start, isBig, stopAtEndOfLine = true)
-            skipOneCharacterBack(offset)
-          }
+          val offset = findNextWordOne(chars, editor, start, isBig, stopAtEndOfLine = true)
+          skipOneCharacterBack(offset)
+        }
+        else {
+          // * Inner object, on word. Move to the end of the current word, do not bother with whitespace.
+          // * Outer object, on whitespace. Include whitespace and the following word by moving to the end of the next
+          //   word/WORD. Newlines are considered whitespace and so can wrap. Make sure that if we are currently at the
+          //   end of a word (because we advanced above) that we do not advance to the end of the subsequent word.
+          findNextWordEndOne(chars, editor, start, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
         }
 
         count--
-      } else {
-        end = pos
       }
 
-      // We cannot rely on the current location of the cursor/"end". If there was no initial selection, then it will be
-      // at the end of a character type block, either word/WORD or whitespace. But if there was an initial selection,
-      // it could be anywhere.
+      // Once we have an initial selection, loop over what's left of count.
+      // * For inner objects, move to the end of the current or next character type block, or the end of line.
+      //   If we're on the last character, it's the next block, otherwise it's the current block. Whitespace is not
+      //   included in the movement/skipped, but does count as part of the loop.
+      // * For outer objects, include whitespace. If we're on a word character, include any following whitespace by
+      //   moving to the character before the next word. If we're on whitespace, move to the end of the next word, which
+      //   includes the preceding whitespace
+      // Note that we can't make any assumptions about the location of `end` at this point. If there was no initial
+      // selection, it will be at the end of a character type block, word/WORD or whitespace. If there was an initial
+      // selection, it's wherever the user selected up to.
       repeat (count) {
-        if (isOuter) {
-          // Outer object. Include whitespace.
-          //
-          // Selection ends on whitespace: Move to end of next word (skips whitespace, including newline)
-          //   "${s}  ${se}      word   " -> "${s}        word${se}   "
-          //   "${s}  ${se} \n   word   " -> "${s}  \n    word${se}   "
-          // Selection ends on end of whitespace:  Move to one character before next word (or end of file)
-          //   "${s}word    ${se}word   " -> "${s}word    word   ${se}"
-          // Selection ends on word: Move to one character before next word
-          //   "${s}wo${se}rd       word" -> "${s}word       ${se}word"
-          //   "${s}wo${se}rd,      word" -> "${s}word${se},      word"
-          // Selection ends on end of word: Move to end of next word (skips whitespace)
-          //   "${s}word${se}    word   " -> "${s}word    word${se}   "
-          // Selection ends on end of word with following word: Move to one character before next word
-          //   "${s}word${se},   word   " -> "${s}word,   ${se}word   "
-          // Selection ends on word char at end of line:
-          //   If next non-newline char is word, move past newline, move to one char before next word
-          //   Else, move to end of next word
-          //   "${s}word${se}\nword     " -> "${s}word\nword     ${se}"
-          //   "${s}  word${se}\nword   " -> "${s}  word\nword   ${se}"
-          //   "${s}word${se}\n  word   " -> "${s}word\n  word${se}   "
-          // Selection ends on whitespace at end of line:
-          //   If next non-newline char is word, move past newline, move to one char before next word
-          //   Else, move to end of next word
-          //   "${s}    ${se}\nword     " -> "${s}    \nword     ${se}"
-          //   "${s}    ${se}\n  word   " -> "${s}    \n  word${se}   "
-          //
-          // This can be generalised to move forward one char, skip again if it's a newline, then either move to the end
-          // of the next word (which skips preceding whitespace), or to one character before the next word (which
-          // includes following whitespace). Moving forward one char means we don't have to distinguish between inside a
-          // word/whitespace, or at the end of a word whitespace. If we started inside, we want to move based on the
-          // current/starting character type, if we started at the end, we want to move based on the next character
-          // type. By always using next, we use the correct character type.
-
-          // Move forward one char
-          // Skip again if new char is newline
-          // If on whitespace, move to end of next word (skips current/preceding whitespace)
-          // If on word, move to one before start of next word (skips following whitespace)
-
-          // Increment, and skip the newline char, unless we've just landed on an empty line
+        // Move forward (and skip end of line char) so we know if we need to move to the current or next word.
+        // If we're at the end of a word, the next character will be a different character type/whitespace.
+        // If we're in the middle of a word, the next character will still be the current word.
+        end++
+        if (end < chars.length && chars[end] == '\n' && !isEmptyLine(chars, end)) {
           end++
-          if (end < chars.length && chars[end] == '\n' && !isEmptyLine(chars, end)) {
-            end++
-          }
+        }
 
-          if (end >= chars.length) {
-            end--
-            return@repeat
-          }
+        if (end >= chars.length) {
+          end--
+          return@repeat
+        }
 
-          end = if (isWhitespace(editor, chars[end], isBig)) {
-            // Move to end of next word (skips current/preceding whitespace)
-            findNextWordEnd(editor, end, 1, isBig, stopOnEmptyLine = true)
-          }
-          else {
-            // Outer object includes whitespace. Starting on a word character, skip to the end of the current word and
-            // then move one character back. Since we're on a word character, we know this isn't an empty line, and we
-            // will therefore always move forward, and so it is always safe to move one character back.
-            val offset = findNextWordOne(chars, editor, end, isBig, stopAtEndOfLine = true)
-            skipOneCharacterBack(offset)
-          }
+        end = if ((!isOuter && isWhitespace(editor, chars[end], isBig))
+          || (isOuter && !isWhitespace(editor, chars[end], isBig))) {
 
-        } else {
-          // Inner object. Whitespace is not included in a move, but included as a separate (counted) move
-          //
-          // Selection ends on whitespace: Move to end of current character type or end of line.
-          //   Or: move to one char before next word
-          //   "${s}  ${se}         word" -> "${s}           ${se}word"
-          //   "${s}  ${se} \n      word" -> "${s}   ${se}\n      word"
-          // Selection ends on end of whitespace: Move to end of next character type.
-          //   Or: move to end of next word
-          //   "${s}           ${se}word" -> "${s}           word${se}"
-          //   "${s}   ${se}\nword      " -> "${s}   \nword${se}      "
-          //   "${s}   ${se}\n      word" -> "${s}   \n      ${se}word" // End of next word doesn't work here
-          // Selection ends on word: Move to end of current character type.
-          //   Or: move to end of current word
-          //   "${s}wo${se}rd       word" -> "${s}word${se}       word"
-          //   "${s}wo${se}rd,      word" -> "${s}word${se},      word"
-          // Selection ends on end of word: Move to end of next character type or end of line.
-          //   Or: move to one before next word, or end of line
-          //   "${s}word${se}    word   " -> "${s}word    ${se}word   "
-          //   "${s}word${se},   word   " -> "${s}word,${se}   word   " // One before next word doesn't work here
-          //   "${s}word${se}  \n  word " -> "${s}word  ${se}\n  word "
-          // Selection ends on word char at end of line: Move to end of next character type SKIPPING NEWLINE!
-          //   Or: move to end of next word
-          //   "${s}word${se}\nword     " -> "${s}word\nword${se}     "
-          //   "${s}  word${se}\nword   " -> "${s}  word\nword${se}   "
-          //   "${s}word${se}\n  word   " -> "${s}word\n  ${se}word   " // End of next word doesn't work here
-          // Selection ends on whitespace at end of line: Move to end of next character type SKIPPING NEWLINE!
-          //   Or: move to one before next word
-          //   "${s}word    ${se}\n    word" -> "${s}word    \n    ${se}word"
-          //   "${s}word    ${se}\nword    " -> "${s}word    \nword${se}    " // Doesn't work
-          //
-          // This can be generalised to move forward on character, skip again if it's a newline, then move to end of
-          // the now current character type.
-
-          // Increment, and skip the newline char, unless we've just landed on an empty line
-          end++
-          if (end < chars.length && chars[end] == '\n' && !isEmptyLine(chars, end)) {
-            end++
-          }
-
-          if (end >= chars.length) {
-            end--
-            return@repeat
-          }
-
-          end = if (isWhitespace(editor, chars[end], isBig)) {
-            // Inner object does not include whitespace, but does count it. Skip to the end of the whitespace by moving
-            // to one character before the next word or end of the current line.
-            // For a non-empty line, it is always possible to move forward and so it is always safe to move one
-            // character back.
-            // Things get weird with empty lines. When handling empty lines above (when there is no initial selection),
-            // we try to get to the character before the next word. We advance, wrap to the next line, and stop because
-            // we're on an empty line (normal before for e.g. `w`). We then come back one character and that puts us
-            // back at the initial offset, and the caret doesn't move.
-            // Vim does things differently if there's an existing selection, and we're moving on to an empty line. The
-            // algorithm needs to see what the next character is, so we move one char forward. This skips us past a
-            // newline char and onto an empty line. We then try to find the next word which automatically advances one,
-            // onto the start of the next line. And now Vim does NOT go back one character, because that would put us
-            // at the newline of the previous line.
-            // Interestingly, because we're not at the start of another line, this one might not be empty. But Vim still
-            // does not move back one, leading to an odd scenario where `iw` can select the *first* character of a word
-            // after whitespace/empty lines. See vim/vim#16514
-            // By refusing to move back even if the current line isn't empty, we're matching Vim's quirky behaviour!
-            val offset = findNextWordOne(chars, editor, end, isBig, stopAtEndOfLine = true)
-            skipOneCharacterBackOnCurrentLine(chars, offset)
-          }
-          else {
-            // Skip to the end of the current word. This would skip preceding whitespace, but we know we're on a word
-            // character.
-            findNextWordEnd(editor, end, 1, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
-          }
+          // * Inner object, on whitespace. Skip the current whitespace, up to the character before the next word.
+          //   For a non-empty line, this will always move forwards, so it is always safe to move one char back.
+          //   For empty lines, things are more complex, and different to the behaviour above, when we set the initial
+          //   range. In that case, we advance while trying to get to the next word, encounter an empty line and stop.
+          //   Then we always move one character back. This can put us back to the original offset (try `viw` on an
+          //   empty line - the caret doesn't move).
+          //   Vim does things differently if there's an existing selection (i.e. this scenario) and we're moving on to
+          //   an empty line. The algorithm advances early (see above) and this skips us past a newline char and on to
+          //   an empty line. We then try to find the next word, which automatically advances a character, on to the
+          //   start of the next line. And now Vim does NOT go back one character, because that would put us at the
+          //   newline char of the previous line.
+          //   You can see this behaviour with `v2iw` on empty lines. Vim selects the first line while initialising the
+          //   range, and then advances 2 lines while handling the second iteration. Similarly, `v3iw` selects 5 lines.
+          //   Interestingly, because we're not at the start of another line, the now-current line might not be empty.
+          //   That means Vim now has a "word" text object that selects just the first character in a line!
+          //   And because we've figured out this difference in handling empty lines, we match Vim's quirky behaviour!
+          //   See vim/vim#16514
+          // * Outer object, on a word character. Move to the end of the current word including following whitespace.
+          //   This is the same as moving to the character before the next word. Also stop at the end of the current
+          //   line. We know this isn't an empty line, so we will never wrap and will always move forward at least one
+          //   character. It is therefore always safe to move back one character, without reaching the start of line.
+          val offset = findNextWordOne(chars, editor, end, isBig, stopAtEndOfLine = true)
+          skipOneCharacterBackOnCurrentLine(chars, offset)
+        }
+        else {
+          // * Inner object, on a word character. Move to the end of the current word. This does not look at whitespace,
+          //   and remains on the current line.
+          // * Outer object, on whitespace. Move to the end of the next word, which will skip the current whitespace.
+          //   Newline characters are whitespace, so this can wrap, although it will stop at an empty line. Make sure
+          //   that if we are currently at the end of a word (because we advanced above) that we do not advance to the
+          //   end of the subsequent word.
+          findNextWordEndOne(chars, editor, end, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
         }
       }
 
@@ -1782,15 +1669,25 @@ abstract class VimSearchHelperBase : VimSearchHelper {
         if (offset > 0 && chars[offset] != '\n') start = offset + 1
       }
 
+      // TODO: Remove this when IdeaVim supports selecting the new line character
+      // A selection with start == end is perfectly valid, and will select a single character. However, IdeaVim
+      // unnecessarily prevents selecting the new line character at the end of a line. If the selection is just that new
+      // line character, then nothing is selected (we end up with a selection with range start==endInclusive, rather than
+      // start==endExclusive). This little hack makes sure that `viw` will (mostly) work on a single empty line
       if (start == end && chars[start] == '\n') end++
+
       return TextRange(start, end + 1)
     }
     else if (!onWordEnd || hasSelection || (count > 1 && dir == 1) || (onSpace && isOuter)) {
       end = if (dir == 1) {
         val c = count - if (onWordEnd && !hasSelection && (!(onSpace && isOuter) || (onSpace && !isOuter))) 1 else 0
-        findNextWordEnd(editor, pos, c, isBig, !isOuter, allowMoveFromWordEnd = false)
+        var c2 = 0
+        repeat(c) {
+          c2 += findNextWordEndOne(chars, editor, end, isBig, stopOnEmptyLine = true, allowMoveFromWordEnd = false)
+        }
+        c2
       } else {
-        findNextWordEnd(editor, pos, 1, isBig, !isOuter, allowMoveFromWordEnd = false)
+        findNextWordEnd(editor, pos, 1, isBig, !isOuter)
       }
     }