Heading

This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
min read

Move a sentence without the clipboard

Word • Macros • Editing
Peter Ronhovde
54
min read

We create a pair of macros to move the current sentence forward or backward in the document without using the clipboard. Sentence movement is intuitive, and several spacing corrections are also made.

Thanks for your interest

This content is part of a paid plan.

Move a sentence in a document

Move a sentence forward or backward in the document? It seems like such a simple task when we can always grab the mouse. Select. Cut. Click. Paste. Done? Right?

Well, kind of … it's better than retyping it, but it’s much more convenient to just tap a single shortcut without having to click-and-tap around to get it done.

Example macro moving a sentence left in a paragraph
Example macro moving a sentence left in a paragraph

It's one of those subtle tasks we do over and over and don't realize we could make it faster and move on with other editing. I suspect you'll be pleasantly surprised at how useful it is.

This extended version improves upon a previous macro to make it work more intuitively as well as be a little smarter with sentence spacing corrections. We’ll focus on the version moving the sentence backward and then adjust it to move forward. Another member version implements it using multiple functions and accounts for dialog.

Create the empty macro

Open the VBA editor and create the following empty macros. When you’re done, you’ll have something like:

Sub MoveSentenceLeft()
' Move the current sentence before the previous without using the clipboard

End Sub

The single quote tells VBA the rest of the text on that line is a comment meant for human readers. We start our macro steps on the empty line.

Important features of the previous macros

The original macros included several fundamental features and solved a few issues we encountered as we created them:

  • Moved a sentence forward or backward in the document (yeah, the whole point)
  • Allowed formatted text but used the clipboard to do so
  • Omitted the paragraph mark from the sentence selection
  • Adjusted some sentence spacing around the moved text
  • Finished with the moved sentence selected (optional)

Omitting the paragraph mark prevented splitting the paragraph when we moved the last sentence. I prefer to select the sentence before finishing the macro as a visual indicator of the change. It’s a little UI tweak that helps a writer think less about what just happened, and it makes it easier to keep moving the same sentence. We’ll keep these features in the improved version, but we’ll also dig deeper.

What are the new features or issues to solve?

What do we plan to improve? Some changes are small but add to the functionality of the macro.

  • Find another way to preserve character formatting other than using the clipboard
  • Correct more spacing issues between sentences and at the end of the paragraph(s)
  • Delete an empty paragraph if the only sentence in the paragraph is moved

The last two additions are small corrections that pop up when the sentence is moved, but we don’t want the writer spending any extra keystrokes adjusting the text after running the macro. The goal is for the macro to “just work,” so we can continue editing without interruption.

Avoid using the clipboard

Macros should not secretly alter the clipboard in the background unless that is the intended action of the macro.

Why?

Imagine we're editing, cutting and pasting text around a scene. We cut some text and then realize a  sentence is out of order, so we use the previous move sentence macro to move it. Then we paste the earlier cut text somewhere else.

Surprise! It's the sentence we just moved.

Arghhh.

It's jarring to paste the wrong text when we didn't cut or copy any new text. If the macro uses the clipboard for the move action, it changes the last entry of the clipboard. VBA doesn't allow us to manipulate the clipboard stack without some major coding shenanigans far beyond the scope of this article. The previously cut text isn't completely lost because Word has a clipboard history, but it's annoying and a waste of time to go back and paste the intended text. How do we get around this?

Use Ranges

We’ll use three working ranges (see our brief introduction to Ranges in Word VBA) to avoid using the clipboard.

  • Source range spans the sentence text to move
  • Target range marks the moved location
  • Temporary range for extra clarity and to avoid changing the others

Range variables provide a tidier way to solve the problem than using the Selection object like the previous macros did. The Range variables also allow us to tweak the sentence spacing more easily since we can work with two different portions of the text without needing to jump around with the Selection to make any changes.

Move a sentence left in the document

We start by defining two working ranges in the document with a goal of copying the formatted text between them.

  • Source range will span the current sentence text to move
  • Target range will begin at the same sentence but be repositioned to the move location

After both ranges are set up properly, we'll copy the formatted text between them and then delete the original text. We'll finish by working through multiple spacing corrections for the two sentence locations.

Define the working ranges

We declare three working ranges for the original sentence, the target position, and an extra temporary range which we explain later.

' Define the working ranges
Dim rSentence As Range, rTarget As Range, rEnd As Range

I often call them "working ranges" simply because we need to work with them to get what we want. Each variable must be given a data type, or it will default to a Variant (generic) type that can store anything. In this macro, the distinction in not important, but giving them a specific data type is clearer. These variables are currently assigned a value of Nothing.

Get the initial sentence range

We can quickly get the initial sentence range from the Sentences collection of the Selection.

' Access the collection of all sentences in the Selection
Selection.Sentences ' Not done ...

The Sentences collection contains the ranges of each partially or fully spanned sentence in the selection. We refer to the First property of the Sentences collection.

' Get the first sentence range of the Selection
Selection.Sentences.First

This returns the Range of the first sentence, so we can assign it to our working range variable rSentence.

' Store the first sentence of the Selection
Set rSentence = Selection.Sentences.First

This assignment ignores any other sentences spanned by the Selection, and it works even if the Selection is an insertion point (i.e., the blinking I-bar waiting for us to type something). This range is may be modified independent of the Selection.

We could instead use Expand paired with the Collapse method, but no difference exists in the final sentence range for purposes of the current macro, and we only need one line for the above assignment. Using Expand would allow us to rewrite the macro to move more than one sentence, but that is outside the scope of this article.

Remove ending paragraph mark(s)

Word will include the paragraph marker by default for the last sentence of a paragraph—

"What? You’re telling me—"

Uh huh, Word automatically considers a sentence range (or an automatic sentence selection) to include any paragraph marks after it.

"Really?"

Yeah, it’s a little odd, so we need to work around it. Let’s remove it from the end of the working sentence range (not the document).

Remove all paragraph marks from the sentence range

Removing a single paragraph mark covers most practical cases, but it's more general to remove all paragraph marks from the end of the sentence range because of how word treats sentence ranges. The relevant Range method is MoveEndWhile.

' Remove all paragraph markers (includes any empty paragraphs)
rSentence.MoveEndWhile Cset:=vbCr, Count:=wdBackward

MoveEndWhile requires a character set, and the command will retract (or extend) the End of the range over any of those characters while it finds at the End position. It stops when it finds any other character not listed in the set. The command is not trying to match the character set as a whole like Find would do. For example, a character set "xyz" would retract the range over any "x" or "y" or "z" in any combination or order until it finds another character.

We assign a paragraph mark character vbCr to the Cset option. Forward is the default, so we need to specify backward using the Count option by assigning it the wdBackward constant. This constant tells the command to move by as many characters as needed (provided the document is smaller than a billion or so characters).

When moving backward, if the End moves past the original Start position for the range, Word will automatically set Start equal to the final End position, so the range effectively collapses. The final position could be outside the original starting range.

Why use MoveEndWhile?

Why not just remove "the" paragraph mark? The original macro used a direct and logical approach where it removed a single paragraph mark character. It checked the last character of the sentence range. If it found a paragraph mark, it used the MoveEnd method to literally retract the Selection by one character to remove it.

The MoveEndWhile command works better for this subtask because we don’t need to worry about how many empty paragraphs follow our sentence range. It’s a fringe case, but it only takes a small change to handle the general case. We even get to shorten the step to one line because the “while” aspect of the MoveEndWhile method handles the conditional aspect for us.

Assign the target range

We've identified the current sentence range at the cursor. The target location for the move is before the previous sentence, so we'll use the rSentence range as a starting point for the target range.

Gotcha when copying range variables

Unfortunately, we can’t just assign the sentence range to the target range in the obvious manner.

' Both ranges will literally refer to the same range
Set rTarget = rSentence ' Does not work as expected

If we try a direct assignment like this, the ranges will refer the same range. Change one, and the other also changes, and it's not just when the spanned content changes. They literally refer to the same range. It’s a little odd that this is default assignment behavior, but that’s how Range assignments work. We want an independent duplicate of the sentence range.

Assign a duplicate range

We need the target range to be independent of the copied sentence range, so we instead refer to the Duplicate property of the sentence range variable.

Set rTarget = rSentence.Duplicate

While the two Ranges currently span the same document text, they are nevertheless independent ranges. Changes to one will not affect the other unless we change the spanned content in some way.

Why did we not need to duplicate the First sentence Range earlier?

It's a little technical, but the First property dynamically generates the desired Range reference every time it is used, so it is essentially an independent Range for the assignment. Referring to the Duplicate method after First would still work though. Using the Duplicate property is important when we're copying an existing range variable.

Move target range to the previous sentence

Now, we can move the duplicated target range to the previous sentence using the Move method.

rTarget.Move Unit:=wdSentence, Count:=-2

We need to move by a sentence, so we assign the wdSentence constant (listed in a table of unit constants) to the Unit option. We also include the count option, Count:=-2, because we need to move backward in the document. A negative value indicates backward, but two sentence steps are necessary because the first step moves to the beginning of the current sentence (effectively collapsing the range). The second step moves the range where we want it.

After the move, rTarget is a collapsed range at the target position one sentence earlier in the document, but no content has moved yet.

Copy formatted text to the target

We need to copy the original sentence text to the target location. We don't want the writer to need to correct anything after moving the sentence. It's counterproductive and just plain annoying, so copying the formatting with the text is required.

We're avoiding the clipboard, so we can't use the Cut method as used in the original macro. We can still preserve the text formatting by using the FormattedText property.

' Copy the formatted text between the ranges
rTarget.FormattedText = rSentence.FormattedText

We assign the FormattedText property using an equals = sign similar to how a numeric assignment is done. This effectively copies the formatted text of the range on the right side to the range on the left. The assignment includes any paragraph formatting if the paragraph marker is included in the range, but we excluded that earlier, so our command will only keep any character formatting.

Use a collapsed target range

Starting with a collapsed target range is important for this command if we want to preserve the content at that location. Otherwise, the assignment would replace any text already spanned by the target range. In this macro, we just want to insert the extra sentence text.

Text property only copies plain text (not used)

We might be tempted just use the Text property, but it only works for plain text.

rTarget.Text = rSentence.Text ' Only works for plain text

This would work for novels in most cases since they tend to include a lot of unformatted text, but even novels occasionally use italics for internal dialog or foreign words spoken in regular dialog.

Delete the original sentence

The original sentence range is still in the document, so we need to Delete it.

rSentence.Delete ' Delete original sentence text

The Delete method works much like the (forward) Delete key on the keyboard. Since the range spans some document content, the command just deletes that text.

The core steps of the macro are done, but we’ll include several improvements and spacing corrections below before we finish.

Select moved sentence (optional)

I prefer to end the macro with the moved sentence selected as a visual clue that something significant changed in the document.

rTarget.Select ' Select moved sentence (optional)

The Select method literally selects the target range contents and scrolls it into view on screen. It also allows the writer to continue moving the same sentence.

Base move sentence macro

Combining the above commands, our base move sentence macro is:

Sub MoveSentenceBackwardBase()
' Move the current sentence backward one without using the clipboard

' Declare working sentence and target ranges
Dim rSentence As Range, rTarget As Range
' Set the working range to the current sentence
Set rSentence = Selection.Sentences.First
' Remove any paragraph marks from the sentence range
rSentence.MoveEndWhile Cset:=vbCr, Count:=wdBackward

' Move the target range back by one sentence
Set rTarget = rSentence.Duplicate ' Begin with the initial sentence
rTarget.Move Unit:=wdSentence, Count:=-2

' Move formatted text to the target location
rTarget.FormattedText = rSentence.FormattedText
rSentence.Delete ' Delete original sentence

' Select moved sentence (optional)
rTarget.Select
End Sub

We’ll extend this with more features and corrections below.

Use intuitive sentence movement

Now, we adapt the base macro to make it act more intuitively—

"Huh? Intuitive?"

Yeah, uhhh, let's start with that I don't like about the base version.

What is the unintuitive behavior?

If we move the first sentence of a paragraph backward, it will move before the last sentence of the previous paragraph.

Move sentence backward example with unintuitive placement
Move sentence backward example with unintuitive placement

The move is technically correct if we count one sentence backward, but it's a little jarring during real editing. The forward and backward movements are also not symmetric. If we follow up by moving the same sentence forward, it becomes the last sentence of the paragraph rather than moving back to where it started.

Include more intuitive movement

We could instead make the first sentence move backward to the last sentence of the previous paragraph? We want the following movement features.

  • If the current sentence is the first of the paragraph, move it to the end of the previous paragraph just before the paragraph mark.
  • If the current sentence is any other position in the paragraph, move it backward one sentence.
  • Moving the last sentence of a paragraph mostly works like any other middle sentence except for needing an extra space correction.

These choices make the sentence movements flow better. When moving backward, the last sentence of the paragraph actually moves like any other middle sentence does, but it will usually not end with a space, so we will need to correct for it.

When moving forward, the last sentence should more intuitively move to the first sentence of the next paragraph, and the first sentence moves like most other middle sentences of the paragraph. The next to the last sentence is another special case we'll consider below.

The original macro inherited the desired sentence behavior based on how the MoveLeft method of the Selection worked around a paragraph mark. In this macro, we need to detect the sentence position and correct the movement based on it because the Move method works a little differently when stepping past a paragraph mark.

Move the first sentence of a paragraph

Since we're modifying the default range movement, we need to know whether the working range spans the first sentence of the paragraph.

How can we detect the first sentence of a paragraph?

To check whether the working sentence range spans the first sentence of a paragraph, we need to know whether it immediately follows a paragraph with no content in between them. In VBA, a paragraph mark is a special character named vbCr as defined in a miscellaneous constants table. The answer is found by testing whether the character just before the sentence range is a paragraph mark character.

We'll ignore the case of the first sentence of the entire document since it is an uncommon use case (trying to move the first sentence backward before nothing?), and the extra validation would clutter the macro.

Detect the first sentence of a paragraph

The character just before the sentence range can be found using the Previous method.

' Get the range of the character just before the sentence range
rSentence.Previous ' Not done ...

The Previous method returns the Range of the previous document unit. The default unit is a character, which is what we need, so we can omit the Unit option. We need the text for the upcoming comparison not the range, so we further refer to the Text property.

' Character is the default unit for the Previous method
PreviousCharacter = rSentence.Previous.Text

Then we store the text character in a string variable since the variable makes the conditional statement below clearer.

First sentence condition based on the previous character

We want to know whether the previous character is a paragraph mark.

' Condition checking whether the previous character is a paragraph mark
PreviousCharacter = vbCr

While it looks like an assignment by itself, this condition is evaluated as a Boolean (True or False) value when it's used in a conditional statement. VBA interprets this condition as a Boolean (True or False) value when it's used in a conditional statement.

If-Then-Else conditional statements

We have a special case and a regular case. Both cases are valid, so we need an If-Then-Else statement.

If SpecialCaseCondition Then
' Do the special case condition steps ...
Else
' Otherwise do the regular steps ...
End If

The steps in the first part are run if the SpecialCaseCondition is True. The Else part is optional, but if it is included, any other cases (meaning SpecialCaseCondition was False) use those steps. See our brief introduction to conditional statements in Word VBA for more explanation.

First sentence rough conditional statement

For this macro, it's easier to verify the special case condition and relegate the other paragraph sentences to the Else part.

' Check whether the sentence begins a paragraph
If PreviousCharacter = vbCr Then
' Current sentence begins a paragraph ...
Else
' Current sentence is somewhere else in the paragraph ...
End If

This works even in the unusual case of a paragraph beginning with spaces because Word automatically includes them as part of the first sentence.

Move to the end of the previous paragraph

The Move command in the base macro above stepped backward by "two" sentences to position the target range before the previous sentence. In this special case, we want to move to the end of the previous paragraph which is literally one character backward in the document. The relevant command is the Move method.

rTarget.Move Count:=-2 ' Move backward past the paragraph mark

The default step size is by a character, so we can omit the Unit option. We need to move backward by one character, but the first step "moves" to the beginning of the sentence (also paragraph) effectively collapsing the range. We again need a Count value of -2.

Choose the correct move command

In the base macro, we always moved back two sentences, but now we have two different target locations. We need to pick the correct location based on the current sentence location in the paragraph. The special case is handled first leaving the regular case of any other sentence in the paragraph as the default.

' Check whether the sentence is the first of the paragraph
If PreviousCharacter = vbCr Then
' Current sentence is the first sentence in its paragraph
' Move to the end of the previous paragraph
rTarget.Move Count:=-2 ' Move back two characters
Else
' Move back one sentence in the document (regular case)
rTarget.Move Unit:=wdSentence, Count:=-2
End If

We'll correct sentence spacing issues below.

Correct sentence spacing

The goal is for the macro to work in most circumstances without needing to manually catch and correct any mistakes. The writer should just be able to keep working uninterrupted after the sentence moves.

Spacing errors are easy to overlook when creating a macro, but the corrections stretch out longer than the base macro. This is unfortunate given their minor role in the task. Skip to the final macros if you wish, or check out the leveled-up member version that implements it with functions instead.

Define simplifying text variables

The spacing logic will be easier to read if we define a few string variables.

' Define some plain text variables for nearby characters
Dim FirstCharacter As String, LastCharacter As String
Dim PreviousCharacter As String, NextCharacter As String

These declarations are not required, but they make the final macro clearer.

Correct spacing after the target position

When we move the last sentence of a paragraph backward, the sentence text at the target location will not include an ending space because the original sentence ended with a paragraph mark instead. We omitted the paragraph mark from the initial sentence range, so we need to add a space after we move it earlier in the paragraph. The catch is the logic needs to be general enough to not add a space at the end of a paragraph.

We'll work with the rTarget range since it spans the moved sentence text.

What are the criteria to add a space after the sentence?

We want to add a space if one is not already present between the two sentences. More specifically, we'll insert the space at the end of the target range but only if that position is not at the end of the paragraph.

Ensure all ending spaces are spanned (just in case)

The beginning of the paragraph allows extra spaces to precede a sentence. If we move the second sentence to that location, the spaces will still be there, but they will then count as part of the moved sentence.

Huh?

Yeah, it's a little awkward, but we may as well catch the oddity by extending the end of the target range over the spaces. The command again uses the MoveEndWhile method as seen in the base macro.

' Extend the range over all ending spaces (just in case)
rTarget.MoveEndWhile Cset:=" "

Admittedly, it's an unusual case to handle, but this command provides more of a logical certainty which is worth the handful of microseconds it requires. After this step, we're sure all spaces are included at the end of the range, so we don't accidentally add an extra one.

Get the last character of the sentence

Now, we check whether a space exists between the moved sentence and the sentence that follows it. We get the last character of the range using the Last property of the Characters collection.

' Last returns the range of the last character not the text
rTarget.Characters.Last ' Not done ...

The Characters collection contains a list of all character ranges in the sentence range. We get the character text from the Text property.

' Get the last character of the sentence range
LastCharacter = rTarget.Characters.Last.Text

We store the text character in an appropriately named String variable.

Condition for whether the last character is not a space

We need to check whether this last character is not a space before we add one. The condition simply compares the variable to a space " ".

' Condition checking whether the last character of the sentence
' range is not a space
LastCharacter <> " "

The not equals <> symbol literally reads "less than or greater than" in the context of numbers, but it's adopted for text with a similar qualitative meaning.

But not at the end of a paragraph

We don't want to insert a space if we've moved the sentence to the end of the previous paragraph. How do we exclude that case?

Ughhh … we also need to check whether the next character after the range is a paragraph mark before adding a space. The Next method works like the Previous method except it gives the next unit range.

' Get the next character after the sentence range
NextCharacter = rTarget.Next.Text

The default unit is a character, and the default number of units is 1, so we can omit both options. A gotcha exists if the range is empty (where it will give the second character after the empty range), but we're mostly sure our sentence range contains text.

Not end of paragraph condition

We don’t want to add a space if we’re already at the end of a paragraph, so we don’t want the NextCharacter to be a paragraph mark.

' Condition checking whether the next character after the sentence
' range is not a paragraph mark
NextCharacter <> vbCr

Using the not equal to <> symbol, this condition checks whether next character is not a paragraph mark.

How can we check two conditions?

How do we check whether both conditions are met at the same time? We use an And operator.

ConditionOne And ConditionTwo

With And in between the conditions, both ConditionOne and ConditionTwo must both be True before the compound condition is True.

Compound condition for when to insert a space

Both character conditions above must be True before we add a space inside the conditional statement.

LastCharacter <> " " And NextCharacter <> vbCr
Insert a space after the target range

We add a space after the range using the InsertAfter method.

' Add the missing space at the end of the sentence
rTarget.InsertAfter Text:=" "

The InsertAfter command does what it says. It adds the given text within the double quotes to the end of the range, and it extends the range variable over that text. Text is the only option, so the command allows us to omit the Text:= part and just give the added text, but it is clearer to keep it.

InsertAfter only adds plain text, but it inherits any formatting at that location (details may vary depending on the local spacing).

Correct spacing after the moved sentence

Putting the sentence space steps together, the conditional statement to ensure proper sentence spacing is:

' Extend the range over all ending spaces (just in case)
rTarget.MoveEndWhile Cset:=" "
' Ensure a space separates the sentences inside a paragraph
LastCharacter = rTarget.Characters.Last.Text
NextCharacter = rTarget.Next.Text
If LastCharacter <> " " And NextCharacter <> vbCr Then
rTarget.InsertAfter Text:=" " ' Add missing end of sentence space
End If

When the conditional statement only contains one command, we'll often condense it to one line, but this one is a little too long for that. This set of steps doesn't correct for extra spaces after the sentence. It just ensures at least one space exists between them.

Correct spacing before the target sentence

If the first sentence of a paragraph is moved into the previous paragraph, it is likely the moved sentence will not have a preceding space. We need to check and correct for that case.

What are the criteria to add a space before the sentence?

We want to add a space if one does not exist before the moved sentence, but we'll need some extra logic to check for the beginning of the paragraph since that is also possible.

Get the previous character

We need the character just before the target range using the Previous method. We again store the character in a conveniently named String variable.

PreviousCharacter = rTarget.Previous.Text

The default Unit of Previous is a character, and the default Count is one, so we can omit both options. A tiny gotcha would occur if the first sentence of the paragraph begins with spaces, but we'll ignore this fringe case here.

Missing previous space condition

We only add a space if the previous character is not a space.

' Condition checking whether previous character is not a space
PreviousCharacter <> " "

Beginning of the paragraph condition

If the sentence is moved to the first sentence of the paragraph, we don't want to insert a space. We can also check whether the previous character is not a paragraph mark.

' Condition for whether the previous character is not a paragraph mark
PreviousCharacter <> vbCr

Compound condition to add a prior space

Both conditions must be true before we add a space, so we use an And operator.

' Compound condition for whether the previous character is not a space
' but also not at the start of the paragraph
PreviousCharacter <> " " And PreviousCharacter <> vbCr

Insert the previous space

We can insert plain text before the range using the InsertBefore method.

' Add a space if it is missing at the end of the sentence
rTarget.InsertBefore Text:=" "

InsertBefore automatically extends the range backward to include the added text.

Remove the space from the target range

We use the target range later in the macro under the assumption that it only spans the moved sentence range. Most sentences in Word begin with the first non-whitespace character, so Word would not usually consider this inserted space to be part of the moved sentence.

We do not want to interfere with any later logic, so we should probably remove the added space from the target range. We only added a single character, so MoveStart is the simplest method.

rTarget.MoveStart ' Remove added space from the target range

MoveStart defaults to moving the Start position of the range forward by a single character which is what we need, so we omit any options.

Correct spacing with previous sentence

Putting the conditions and the commands into a conditional statement, we have:

' Correct for a missing spacing with the previous sentence
PreviousCharacter = rTarget.Previous.Text
If PreviousCharacter <> " " And PreviousCharacter <> vbCr Then
rTarget.InsertBefore Text:=" " ' Add missing end of sentence space
rTarget.MoveStart ' Remove added space from range
End If

Delete an empty paragraph (optional)

What if we moved the only sentence in a paragraph leaving it empty?

If you don’t care about occasional empty paragraphs appearing, then there’s nothing to do here. I find it a little unsightly to leave a blank line after the move, so let’s delete it.

What are the criteria for detecting an empty paragraph?

The rSentence working range is already at the potentially empty paragraph since we deleted the original sentence. We’ve already deleted all text and any associated spaces, so we can check for an empty paragraph by looking for consecutive paragraph marks, a vbCr followed by a vbCr, with no text in between them.

Basically, we need to know whether the previous character and the first character of the empty range variable are both paragraph marks.

Assign the empty paragraph test characters

We redefine the previous and next character variables for this new test.

PreviousCharacter = rSentence.Previous.Text
FirstCharacter = rSentence.Characters.First.Text

We need the First character of the sentence range since we are sure (given our previous steps) the rSentence range is empty. This gives us the intuitive next character for an empty range.

Range text gotcha with empty ranges

Unfortunately, when the range is empty (no text is spanned), the “next” character in the intuitive sense is actually found using the “first” character of the empty range. This is a little confusing since it’s not actually spanning any text (the Selection behaves a little differently). For our purposes, we just need to pick the correct character.

Compound condition to detect an empty paragraph

Both paragraph mark conditions must be True, so we again need the And operator.

' Compound condition checking for an empty paragraph (assumes the
' range is empty)
PreviousCharacter = vbCr And FirstCharacter = vbCr

Conditional statement to delete an empty paragraph

Putting this compound condition into a conditional statement, we have:

' Check whether we have an empty paragraph and delete it if so
If PreviousCharacter = vbCr And FirstCharacter = vbCr Then
rSentence.Delete ' Delete the empty paragraph
End If

We simply Delete the paragraph mark character to the right of the empty range which removes the empty paragraph from the document.

Delete extra spaces at the end of the source paragraph

Suppose we move the last sentence (in the sense of order, not the only one) of the source paragraph. After the original text is deleted, the previous sentence will probably leave a space at the end of the paragraph. We need to delete it.

Detect the end of the paragraph

The rSentence working range is already positioned at the end of the paragraph. The original sentence text was deleted earlier, so rSentence is currently empty. A space may or may not be present just to the left of the range with a possible paragraph mark to the right. The catch is we don't want to delete the space if the sentence is somewhere else inside the paragraph.

Before we delete anything, we need to test whether the next character is a paragraph mark or not.

Store the first character after the range

For an empty range, the intuitive next character is identified using First character of the Characters collection.

FirstCharacter = rSentence.Characters.First.Text

We store the character in an appropriately named String variable. Using the first character is necessary because the "next" character as VBA sees it using the Next method is actually the second character after an empty range.

First character comparison

Our end-of-paragraph condition checks whether this first character is a paragraph mark character vbCr.

' End of paragraph condition based on empty range text
FirstCharacter = vbCr

Extend over the extra spaces

If we have a paragraph mark on the right, then we extend the range over all spaces to the left in preparation for deleting them. The relevant command is the MoveStartWhile method.

' Extend the range backward over any nearby spaces
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward

MoveStartWhile works just like MoveEndWhile from earlier except it moves only the Start position of the Range variable. We assigned a space character " " in double quotes to the Cset option, so this command will only extend the range backward over spaces.

In most cases, only one space would linger at the end of the paragraph, but we might as well handle the general case using the MoveStartWhile command to extend over all spaces. It also simplifies this step since the command handles the conditional logic for us.

Delete any extra spaces

The Delete method is simple. If the range spans any content, Delete will remove it from the document much like what happens when we tap the Delete key.

rTarget.Delete ' Delete spanned spaces?

The method has some options, but it's usually used just as a plain ole Delete.

But we have a problem

If no spaces are present, the Delete command will still delete something. Specifically, it would delete the character to the right of the range just like the (forward) Delete key would do.

Ughhh.

We only want to delete extra spaces if they exist, so in addition to checking for a paragraph mark, we need to make sure at least one space exists to the left of the empty sentence range before deleting anything.

Check for an existing space

We need to know if the range had any preceding space before we try to extend over and delete them. We could validate whether the MoveStartWhile command found any spaces, but it gets a little messy.

What then?

We're already validating the paragraph mark character, so the easiest way is to check for a space at the same step before the extension command runs. We again refer to the Previous method to get the character just before the empty sentence range.

PreviousCharacter = rTarget.Previous.Text

The default unit and count for Previous is a single character, so we omit the options. Then we need the text of the character range, so we refer to the Text property.

Compound condition to detect an end of paragraph space

We only delete the spaces if we have a space on the left and a paragraph mark on the right. Since both must be True, we use the And operator again.

' Compound condition verifying any spaces at the end of a paragraph
PreviousCharacter = " " And FirstCharacter = vbCr

The first character variable assumes an empty range.

Delete end of paragraph spaces conditional statement

Inserting the above condition and the range extension command into an If statement, we have:

' Delete any end of paragraph spaces at the source sentence
If PreviousCharacter = " " And FirstCharacter = vbCr Then
' Extend over and delete all ending paragraph spaces
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward
rSentence.Delete

' But we have another problem ...
End If

I wish I could say we're done, but … not quite.

Ughhh.

But we can't quit, or we'll lose that zing of accomplishment when we have a working macro that helps us edit faster.

Correct a “helpful” Word feature

Sometimes Word adjusts spacing for us which may include deleting an extra space or inserting a space Word thinks is missing. This feature is often convenient in everyday writing and editing. We occasionally don’t need to manually insert or remove a space when we paste text, for example, but it can be annoying when creating macros.

Since the sentence range is currently at the end of the paragraph, we should be able to just delete the excess space(s) and be done, but testing shows Word reinserts a space even though it's at the end of the paragraph.

Arghhh.

Word should be a little smarter about reinserting a space since requiring one at the end of the paragraph doesn't make sense. We can, however, add a few extra steps to detect and correct for it despite Word’s attempt to help us.

Redefine the first character

Word will reinsert the space to the right of the range after we delete the others, so we reassign the first character variable based on the current empty sentence range (no reason to create a new variable).

FirstCharacter = rSentence.Characters.First.Text
Check for a space again

This time we’re checking whether it’s a space Word reinserted automatically or not.

' Check whether Word reinserted a space at the end of the paragraph
If FirstCharacter = " " Then
rSentence.Delete ' Delete extra space Word reinserted
End If

A simple Delete command deletes the space to the right of the empty range. The conditional statement is simple enough that we can condense it onto one line with the Delete method.

' Delete straggling space if Word adds it
If FirstCharacter = " " Then rSentence.Delete

Unfortunately, this needs to go inside the earlier If statement, but the condensed form makes it more readable.

Wouldn’t it be simpler if we always delete the space?

Why not just delete the space every time?

' Why not just do this instead? (not used)
If PreviousCharacter = " " And FirstCharacter = vbCr Then
' Select and delete all ending paragraph spaces
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward
rSentence.Delete
rSentence.Delete ' delete reinserted space?
End If

After all, we “know” it’s there, right?

Uhhh … well, kind of, but I can't guarantee that Word will always insert it for everyone that uses this macro.

I'm not sure because it depends on the Use smart cut and paste setting. I prefer to make sure the space is present before trying to delete it. It's probably not a big deal since it would just delete one character, but it only requires a handful of microseconds to check, so why not be sure?

Finally delete the spaces

Here is the result for deleting any excess end of paragraph spaces.

' Delete any end of paragraph spaces at the source location
PreviousCharacter = rSentence.Previous.Text
FirstCharacter = rSentence.Characters.First.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
' Select and delete all ending paragraph spaces
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward
rSentence.Delete

' Delete a straggling space if Word reinserts it
FirstCharacter = rSentence.Characters.First.Text
If FirstCharacter = " " Then rSentence.Delete
End If

A seemingly simple correction grew into a pile of macro steps.

Delete extra spaces at the end of the target paragraph

With the more intuitive sentence movement mentioned earlier, we required the first sentence of the current paragraph to move backward to the end of the previous paragraph.

Okay …

The moved sentence probably has a trailing space that moves with it. It's now the last sentence of its new paragraph, so we need to delete the trailing space like we did for the source paragraph earlier. This is a good example of how seemingly small changes to one part of a macro affect other parts. Fortunately, we can reuse the previous steps where we deleted extra spaces at the end of the source paragraph.

Reuse the prior solution

Before we copy the steps from above, but we need to change the range variable from the sentence range, rSentence, to the moved sentence range. Other than removing the extra space, we don’t want to disturb the current target range, rTarget, since it spans our desired sentence text at the new location.

Copy the current target range

We need to store a temporary copy of the rTarget range in a new variable. We'll call it rEnd. We again refer to the Duplicate property, so we have an independent range variable.

Dim rEnd As Range ' Or just include it with the previous ranges
Set rEnd = rTarget.Duplicate

In the macro below, we included the rEnd variable declaration with the source and target range variables to save a step.

Collapse the new end range

The previous steps worked with a collapsed sentence range because we deleted the original sentence. We don't want to delete the target range, so we need to collapse its copied range, rEnd. We'll Collapse the range toward the End position using the Direction option.

rEnd.Collapse Direction:=wdCollapseEnd

Technically, wdCollapseEnd comes from a small table, but only two values are in table.

Modified steps with a copy of the target range

We now have an empty range positioned at the end of the moved sentence text, so we can delete any excess spaces if it is at the end of its paragraph. Aside from the duplication and collapse steps to set it up, we simply modify the earlier steps by swapping out rSentence for rEnd.

' Set a duplicate of copied sentence range
Set rEnd = rTarget.Duplicate
' Work with end of copied target range
rEnd.Collapse Direction:=wdCollapseEnd

' Delete any end of paragraph spaces at the target location
PreviousCharacter = rEnd.Previous.Text
FirstCharacter = rEnd.Characters.First.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
' Select and delete all ending paragraph spaces
rEnd.MoveStartWhile Cset:=" ", Count:=wdBackward
rEnd.Delete

' Delete straggling space Word reinserts sometimes
FirstCharacter = rEnd.Characters.First.Text
If FirstCharacter = " " Then rEnd.Delete
End If
This is a why functions exist

The repeated steps illustrate why functions were created and are even available in a "scripting" language like VBA (although it's a genuine programming language). We literally just copied the same steps over and made a trivial change to apply them to a different range. Plus, this macro is stretching out longer than the Golden Gate bridge, so implementing a few functions would also make it more readable. Another member article breaks the macro down into smaller, bite-sized chunks.

Differences with the move forward macro

Many similar details occur in both macros. The initial sentence expansion and the spacing corrections are done either way, but what are the differences?

Obviously, the different direction changes how we determine the target location, but another special case pops up for the second to the last sentence of a paragraph. It’s a subtle distinction, so when creating your own macros with slight variations, don’t assume everything is the same. Test your macros.

Catching the next to last sentence of the paragraph

We have another special catch to catch with the next to last sentence of the paragraph.

Why is this sentence a separate case?

In an effort to make the sentence movement more intuitive, we insisted sentences at either side of a paragraph move between paragraphs as a separate step—last sentence moves to first of the next paragraph (forward) or the reverse if moving backward—but we skipped over this case. When moving forward, the macro should move the next to the last sentence to the last sentence of the current paragraph.

What's does the distinction matter?

The Move method (by a sentence) would move the target range to the beginning of the second sentence after the starting position, but that is also where the next paragraph begins. This is not the intended movement, so let's fix it.

Get the last character of the next sentence

This check is messier than the others because we begin with a sentence in the "middle" of a paragraph. We've been using the paragraph mark character vbCr to detect the first or last sentence of a paragraph, so we need some extra VBA machinery to get the desired character for our test.

Get the next sentence range

We need the next sentence, so we again refer to the Next method of the target range.

rSentence.Next ' Not done ...

This time, we need the next sentence, so we can't omit the Unit option. Moreover, we're referencing something else afterward, so we need to put the Unit option in parentheses (or VBA will complain that it doesn't understand our command). We assign the sentence unit constant, wdSentence, to the Unit option.

' Get the range of the next sentence
rSentence.Next(Unit:=wdSentence) ' Still not done ...

This statement returns the Range of the next sentence, but we need the last character of that sentence. We'll store this range in our temporary range variable, rEnd, just to keep the line length reasonable.

' Temporarily store the next sentence range
Set rEnd = rSentence.Next(Unit:=wdSentence)
Get the last character

As we've done before, we refer to the Last character range of the Characters collection. The Text property again gives us the actual last character.

' Store the last character of the next sentence range
NextSentenceLastCharacter = rEnd.Characters.Last.Text

We store this character in another text variable. The long variable name is just for extra clarity.

Condition for next to last sentence of the paragraph

If this last character is a paragraph mark, then our original sentence should be the next to last sentence but see the small gotcha below.

' Condition testing whether the next sentence is the last sentence
' of the current paragraph
NextSentenceLastCharacter = vbCr
Sidestep a gotcha? (uhhh … never mind)

There is a potential problem if the cursor is in the last sentence of a paragraph but just before a single-sentence paragraph.

Ughhh … does it ever end?

It's not as bad as it seems. We just need to be sure the cursor is not already inside the last sentence of the paragraph, or we could get a false positive.

Fortunately, the above condition check immediately follows the last sentence check. Since we've already tested for the last sentence of the paragraph, we can skip the extra condition here. In rough logic, it would look something like:

If the sentence is the last one in its own paragraph Then
' Do these steps for the last sentence of a paragraph ...
ElseIf the sentence is next to last in its own paragraph Then
' Do these steps for the next to last sentence of a paragraph ...
' Last sentence case had to be False to get here, so we do not need
' to recheck that condition.
Else ' Otherwise ...
' Do these steps for any other sentence in the paragraph ...
End If

With that caveat, we can proceed with the simpler condition. It's not a big gotcha case, but it illustrates the kinds of things we need to think some about when creating robust editing macros. See our brief introduction to conditional statements in VBA for more explanation of ElseIf, but is basically just allows us to chain together extra conditions when needed. The steps are only run for the first condition that is True.

Move target range to the end of the paragraph

We need to move the target range to the end of the paragraph. Several methods exist, so we'll just pick an obvious one. We first move to the end of the paragraph using the Move method but with a paragraph unit constant, wdParagraph.

' Move down by one paragraph
rTarget.Move Unit:=wdParagraph

The default Count is by one unit, so we can omit that option. Technically, this moves to the beginning of the next paragraph since that is also the end of the current paragraph, so we need to tweak the position by moving backward one character.

' Move backward to the end of the original paragraph just before
' the paragraph mark
rTarget.Move Count:=-1

The default unit is by a character, so we omit that option. We need a negative Count value to indicate moving backward by one character. The target range is now positioned at the end of the original paragraph, so the macro can proceed with the rest of the macro.

Final move sentence macros

Let’s put these changes together into a pair of revised, more intuitive, but annoyingly long macros. If I must choose between brevity or having the macros do what I want. Improved functionality and usability win almost every time.

They're not overly complex, just ungainly, but they provide a good excuse to introduce functions in a follow-up member article. The different spacing corrections can be neatly separated out and would probably apply to other macros later. As a school of hard-knocks example on the utility of functions, these macros serve that secondary purpose well.

Moving sentence backward

Merging the above commands, our macro to move a sentence backward in the document is:

Sub MoveSentenceBackward()
' Move the current sentence backward one without using the clipboard

' Declare several plain text variables for corrections below
Dim FirstCharacter As String, LastCharacter As String
Dim PreviousCharacter As String, NextCharacter As String

' Declare several working ranges
Dim rSentence As Range, rTarget As Range, rEnd As Range
' Store the first sentence of the Selection to move backward
Set rSentence = Selection.Sentences.First
' Remove any paragraph marks from the sentence range
rSentence.MoveEndWhile Cset:=vbCr, Count:=wdBackward

' Set the initial working target range to the original sentence
Set rTarget = rSentence.Duplicate

' Move the target location back one sentence or to the end
' of the previous paragraph
' Check for the beginning of the current paragraph
PreviousCharacter = rTarget.Previous.Text
If PreviousCharacter = vbCr Then
' Move past the previous paragraph mark
rTarget.Move Count:=-2
Else
' Move back one sentence in the document (regular case)
rTarget.Move Unit:=wdSentence, Count:=-2
End If

' Move the formatted text to the target location
rTarget.FormattedText = rSentence.FormattedText
rSentence.Delete ' Delete original sentence

' Ensure a space separates sentences inside a paragraph
' Extend the target range over all ending spaces (just in case)
rTarget.MoveEndWhile Cset:=" "
' Correct for a missing end of sentence space at the target location
LastCharacter = rTarget.Characters.Last.Text
NextCharacter = rTarget.Next.Text
If LastCharacter <> " " And NextCharacter <> vbCr Then
rTarget.InsertAfter Text:=" " ' Add missing space
End If

' Correct for a missing start of sentence space at the target location
PreviousCharacter = rTarget.Previous.Text
If PreviousCharacter <> " " And PreviousCharacter <> vbCr Then
rTarget.InsertBefore Text:=" " ' Add missing space
rTarget.MoveStart ' Remove added space from the range
End If

' Delete empty paragraph if we moved the only sentence
PreviousCharacter = rSentence.Previous.Text
FirstCharacter = rSentence.Characters.First.Text
If PreviousCharacter = vbCr And FirstCharacter = vbCr Then
rSentence.Delete ' Delete the empty paragraph
End If

' Delete any end of paragraph spaces at the source sentence
' First character of rSentence detects the end of the paragraph because
' it is already collapsed from the delete above
PreviousCharacter = rSentence.Previous.Text
FirstCharacter = rSentence.Characters.First.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward
rSentence.Delete ' Delete preceding spaces

' Delete straggling space Word reinserts sometimes
FirstCharacter = rSentence.Characters.First.Text
If FirstCharacter = " " Then rSentence.Delete
End If

' Delete any end of paragraph spaces at the target location
' We do not want to change rTarget, so create a temporary duplicate
Set rEnd = rTarget.Duplicate
' Work with the end of the moved sentence range
rEnd.Collapse Direction:=wdCollapseEnd
PreviousCharacter = rEnd.Previous.Text
FirstCharacter = rEnd.Characters.First.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
rEnd.MoveStartWhile Cset:=" ", Count:=wdBackward
rEnd.Delete ' Delete preceding spaces

' Delete straggling space Word reinserts sometimes
FirstCharacter = rEnd.Characters.First.Text
If FirstCharacter = " " Then rEnd.Delete
End If

' Finish with the moved sentence selected (optional)
rTarget.Select
End Sub

Some spacing corrections can be done out of order, but it's not a free for all.

Moving sentence forward

The move sentence forward version of the macro is a little longer because of an extra sentence case. We also need to adjust the corresponding steps (performed off screen) for moving forward in the document, so the target range movement details are a little different. The rest of the macro is very similar to the backward version.

Sub MoveSentenceForward()
' Move the current sentence forward one without using the clipboard

' Declare plain text variables mostly for corrections below
Dim FirstCharacter As String, LastCharacter As String
Dim PreviousCharacter As String, NextCharacter As String
Dim NextSentenceLastCharacter As String

' Declare several working ranges
Dim rSentence As Range, rTarget As Range, rEnd As Range
' Store the first sentence of the Selection to move forward
Set rSentence = Selection.Sentences.First
' Remove any paragraph marks from the sentence range
rSentence.MoveEndWhile Cset:=vbCr, Count:=wdBackward

' Set the initial working target range to the original sentence
Set rTarget = rSentence.Duplicate

' Move the target location forward one sentence or to the beginning
' of the next paragraph
' Store the last character of the next sentence
Set rEnd = rTarget.Next(Unit:=wdSentence) ' Temporary last sentence range
NextSentenceLastCharacter = rEnd.Characters.Last.Text
' Check for the end of the current paragraph
NextCharacter = rSentence.Next.Text
If NextCharacter = vbCr Then
' Move to the beginning of the next paragraph
rTarget.Move Unit:=wdParagraph
ElseIf NextSentenceLastCharacter = vbCr Then
' Next to last sentence of the paragraph, so position the target range
' at the end of the initial paragraph
rTarget.Move Unit:=wdParagraph ' Move to next paragraph
rTarget.Move Count:=-1 ' Move back past paragraph mark
Else
' Move forward one sentence (regular case)
rTarget.Move Unit:=wdSentence, Count:=2
End If

' Move formatted text to the target location
rTarget.FormattedText = rSentence.FormattedText
rSentence.Delete ' Delete original sentence

' Ensure a space separates the sentences inside a paragraph
' Extend target range over any ending spaces (just in case)
rTarget.MoveEndWhile Cset:=" "
' Correct for a missing end of sentence space at the target location
LastCharacter = rTarget.Characters.Last.Text
NextCharacter = rTarget.Next.Text
If LastCharacter <> " " And NextCharacter <> vbCr Then
rTarget.InsertAfter Text:=" " ' Add missing space
End If

' Correct for missing start of sentence space
PreviousCharacter = rTarget.Previous.Text
If PreviousCharacter <> " " And PreviousCharacter <> vbCr Then
rTarget.InsertBefore Text:=" " ' Add missing space
rTarget.MoveStart ' Remove added space from the range
End If

' Delete empty paragraph if we moved the only sentence
PreviousCharacter = rSentence.Previous.Text
FirstCharacter = rSentence.Characters.First.Text
If PreviousCharacter = vbCr And FirstCharacter = vbCr Then
rSentence.Delete ' Delete the empty paragraph
End If

' Delete any end of paragraph spaces at the source sentence
' First character of rSentence detects the end of the paragraph because
' it is already collapsed from the delete above
FirstCharacter = rSentence.Characters.First.Text
PreviousCharacter = rSentence.Previous.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
rSentence.MoveStartWhile Cset:=" ", Count:=wdBackward
rSentence.Delete ' Delete preceding spaces

' Delete straggling space Word reinserts sometimes
FirstCharacter = rSentence.Characters.First.Text
If FirstCharacter = " " Then rSentence.Delete
End If

' Delete any end of paragraph spaces at the target sentence
' We do not want to change rTarget, so create a working duplicate
Set rEnd = rTarget.Duplicate ' Reuse temporary range variable
' Work with the end of the moved sentence range
rEnd.Collapse Direction:=wdCollapseEnd
PreviousCharacter = rEnd.Previous.Text
FirstCharacter = rEnd.Characters.First.Text
If PreviousCharacter = " " And FirstCharacter = vbCr Then
rEnd.MoveStartWhile Cset:=" ", Count:=wdBackward
rEnd.Delete ' Delete preceding spaces

' Delete straggling space Word reinserts sometimes
FirstCharacter = rEnd.Characters.First.Text
If FirstCharacter = " " Then rEnd.Delete
End If

' Finish with moved sentence selected (optional)
rTarget.Select
End Sub

I assigned my versions of these macros to Option+Shift+Left or Right arrows in Word for Mac or Alt+Shift+Left or Right arrows on Windows. These shortcut combinations aren’t perfect since I’d prefer those key combinations extend the selection by sentences (akin to Command+Shift+Left or Right arrows for words), but it’s the best solution so far for me since I move sentences more than I need to select them.

Comment on comments

Notice the regular comments throughout both macros. They would be even more difficult to follow without the additional explanations.

I generally explain any notable individual steps, chunks of steps, what variables mean, or just what I was thinking at the time if it might not be obvious later. I’ll also note any specific issues I was having such as why something didn’t work or why I chose a particular solution. If a strange step is required, I’ll add a note about why it was included.

You’ll thank yourself for including comments if you ever go back to the macro later. Multiple times, I’ve forgotten what I was thinking or how I was solving a problem at the time, and the comments saved me time fixing or enhancing the macro.

Improvements

This macro needs further improvements, but given the already lengthy macros, they're omitted from the above versions.

More spacing tweaks

As I test the macros, I see a few places where I would prefer my versions further tweak other spacing oddities. Given the fringe nature of the extra corrections, they're omitted in the above macros.

Disable screen updates

With the multiple changes, the macros should probably disable screen updates while running. It sometimes results in a cleaner edit rather than seeing twitchy changes on the screen.

Add an undo record

With multiple possible changes in sequence, we should probably add an Undo record. However, include them with caution because they can cause problems with the undo actions, and they can even occasionally (rarely) confuse Word's cloud syncing.

Allow dialog?

Since I also write novels, I prefer my editing macros naturally handle dialog at a minimum, but the macros are already so long. That is a lesson for another day.

Functions please!

The steps for the sentence range extension, spacing corrections, and deleting an empty paragraph are all respectively the same. The latter commands stretch out until the simple corrections form the bulk of the macro. These spacing adjustments will pop up in other macros, so they beg to be extracted into separate functions. We already created a function to do the sentence selection, and another member version uses the other mentioned functions. We end up with a cleaner macro and several extra tools for our macro toolbox.

Affiliate Links

If you're interested in using Word or another tool related to the article, check out these affiliate links. I may make a small commission if you purchase when using them, but there is no increase in cost for you, and it helps to support this site and associated content.

I've been using Microsoft for Business for commercial use (that's us writers) on one of the lower pricing tiers for years. I get to use my macros, have online storage, and don't have to worry about software updates.