Move a sentence forward or backward in the document with a keystroke?
This is one of those macros you feel like you’d never need. You can always grab the mouse, right? But it’s refreshing when you just get it done and don’t have to click-and-tap around to do it.
Thanks for your interest
This content is part of a paid plan.
Create the empty macro
A previous post covers creating an empty macro like the one shown below. When you’re done, you’ll have something like:
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.
Of course, create a second one to move the current sentence by one sentence right in the document.
Improving the sentence macro
Today, we’re improving a previous macro to make it act more intuitively and robustly.
The explanations overlap some with the simpler version, but this one addresses the problem using ranges and goes into more detail while solving a few other gotchas and special cases along the way.
If you like optimizing your macros so they behave even more naturally and intuitively while editing, read on.
We’ll focus on the move-backward macro and adjust for movement forward at the end. The work required for this one is more than most we’ve done, but I’m guiding you through the process the whole way.
Fair warning
This is the longest macro we’ve completed to date, but rather than infantilize it, we’ll work through it with a nearly full implementation (omitting only handling dialog and parentheses naturally). Then in upcoming articles, we’ll use it as a launching point to show why we can and should make better macros using functions.
Base macro
The original macro included several features and solved some issues we encountered as we created it:
- Moved a sentence backward in the document with a keystroke (the whole point)
- Allowed formatted text using the clipboard
- Omitted the paragraph mark from the sentence selection which prevented splitting the paragraph when we moved the last sentence
- Adjusted some sentence spacing when necessary
We’ll keep these features in the new and improved version, but we’ll also dig deeper.
What are the new issues to solve?
What do we plan to improve? Some changes are small but still add to the functionality of the macro.
- Include more intuitive sentence movement specifically when moving sentences between paragraphs
- Find another way to preserve character formatting other than using the clipboard
- Correct spacing between sentences and at the end of a paragraph as needed
- Delete an empty paragraph after moving the only sentence
- Finish with the moved sentence selected
I like ending the macro with the sentence selected as a visual indicator of the change. It’s just a little UI tweak that helps the user think less about what just happened.
Two other changes incorporate small text corrections due to the moved text, so we don’t have to do anything extra after using the macro. We don’t want to spend extra keystrokes adjusting the text after running the macro.
The goal is for the macro to “just work,” so the user can continue editing without interruption.
Solutions
Two main structural changes for this macro include:
Avoiding the clipboard
Not using the clipboard is more of a preference, but I don’t like my macros mysteriously altering the clipboard in the background. In VBA, it is difficult to save and restore the clipboard contents. If the user had something there, it’s essentially gone assuming they don’t use the multi-item clipboard feature in Word.
How do we get around this?
Using Ranges
We’ll use two ranges and copy the formatted text between them (see earlier brief introduction to ranges). Ranges are a tidier way to solve the problem than using the Selection object. They also allow us to tweak a couple other minor features easily.
Getting started
We start by defining two working ranges for the current sentence in the document with a goal to copy formatted text between them.
Set up the initial sentence Range
We get the user’s starting position in the document using the Selection’s Range property.
Recall we have to “Set” a range because it holds more information than just a value like a number or text.
Collapse the range
Collapse the sentence range, so we don’t have to worry about any issues with a starting selection.
Now the range spans no text, so the Start and End positions are the same.
This collapse does not change the actual Selection in the document since we’re working with a range variable.
This isn’t strictly necessary, but if there were a starting selection that spanned sentences, then the macro would move all connected sentences as a group. Not really a bad feature to allow, but it is slightly more than expected. If you like it as an extra feature, then omit this step.
Expand over sentence
Expand the rSentence Range over the current sentence.
Recall the Expand command automatically expands the current range to include the beginning and end of the given Unit in the document. It uses Unit constants which includes the common ones we consider when creating documents (e.g., words, sentences, paragraphs, etc.). Each constant is preceded with a “wd” to denote it as a word constant.
The Expand command will not grow the Selection beyond the Unit specified. This differs from how it’s used on the keyboard with F8 which grows the selection by successive unit sizes when you press the key again.
We specify command options using an option name, which is Unit here, followed by a := before we give the value.
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—
Yes, Word selects all empty paragraph marks after an expanded sentence range.
Really?
It’s a little odd to me, but we need to work around it, so let’s remove it from the working Range (not the document).
Simple method
The simple approach, which was used in the original macro, is to just remove a single paragraph mark character. It would be the very last character in the expanded selection using the Characters collection of the range.
The Last character here is a range in the document, but we want the Text for that range, so we add the Text property.
We don’t have to assign the character to a variable like we’ve done here, but it makes the macro easier to read.
Now we test whether it is a paragraph mark or not. The paragraph mark is a special character, so we refer to it using the constant vbCr.
The MoveEnd command adjusts the End position of the rSentence range by one character backward since Count:=-1. The default Unit (step size) is by character, so we didn’t need to specify it.
General method
I prefer to remove all paragraph marks from the end of the selection.
This is a little nicer because we don’t have to worry about multiple empty paragraphs following our selected sentence. It’s a fringe case, but it only takes a small command change to handle the general case.
We’ve encountered the MoveEndWhile command before. It keeps moving the End position of the Range as long as it keeps finding any characters listed in the Cset option. It is not trying to match the character set as a whole like using Find would do.
Here we’ve given Cset as a special character for the paragraph mark. In general, we would include any characters in double quotes like Cset:=“abc”.
The Count:=wdBackward option does what it implies. It tells the command to move End backward in the document since forward is the default.
One constraint is the End position cannot move earlier than the Start position for the range, so Word will automatically set them equal if End gets to the Start position.
Interestingly, we don’t even need an If statement like we used above because the “while” part of the MoveEndWhile command handles the condition for us.
Create a second range
Now that we have our sentence selected, we need a copy of it which we’ll move to the target location. The above range marks the sentence we want to move, so we begin there.
Gotcha with copying ranges
Unfortunately, we can’t just set the range in the obvious manner.
If you try a direct assignment like this, the ranges will literally be the same thing. You change one, and the other one also changes. It’s a little odd to me as a default assignment behavior, but that’s how Range assignments work.
How to properly duplicate ranges
We need to be able to change the copied range independent of the first one. To accomplish this, we set rCopy to a duplicate of the sentence Range
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 insert or remove text.
Move to previous sentence location
Now that we have a duplicate range rCopy, we can move it to the previous sentence using the Move command
Obviously, we need to move with a wdSentence Unit, but we use the Move command with a Count:=-2 because negative is backward in the document. The 2 is because the first sentence step just moves to the beginning of the current sentence.
After the move, rCopy is a collapsed range at the target position one sentence earlier in the document.
Copy sentence
We’re avoiding Cut and Paste here since they use the clipboard. We might be tempted just set the Text properties equal
This works if the copied text is plain text with no formatting. This is probably fine for most novels since novels tend not to use a lot of formatted text, but even they occasionally use italics.
We want the macro to be general purpose with no extra work, so we instead with use the Range’s FormattedText property.
In general, this command would also include any paragraph formatting if the paragraph marker is included in the Range, but we excluded that earlier, so our command will only keep the character formatting, if applicable.
Include ending spaces
Word usually includes trailing spaces after an automatic selection or extension, so we’ll mimic that behavior for consistency
We don’t specify a direction since forward is the default.
This step also makes checking for a missing space between sentences (coming up later) a little easier since we know we’ve included any that are present at the end of the copied sentence.
Delete the original sentence
The original sentence range is still unchanged by the macro, so we can delete it now.
The main content of the macro is done, but we’ll include several improvements and corrections below.
Base Macro
The new base macro using ranges as follows. We’ll extend this with new features and corrections coming up.
I do not include a forward version since this is intended as an intermediate macro.
Is that all?
Often you can start with a working macro but then realize you want it to do X or Y or handle some other annoying special case automatically.
Forewarning
Sometimes accounting for special cases or fixing annoying gotchas can be harder than creating the core macro. This move sentence macro is one of those.
It’s one of the longer editing macros I’ve created, but I insist my macros work intuitively with little extra action on my part. If you want to follow along, just be aware it’s a slog in places.
Fixing Gotchas
Before we start tweaking the movement behavior of the macro, we need to catch and correct special cases to match what we expect in normal usage.
Delete extra spaces at end of source paragraph
Suppose we move the last sentence of the source paragraph. After the original text is deleted, the previous sentence will probably leave a space at the end of the paragraph which is annoying.
Detecting the end of the paragraph
To detect whether this is the case, we use the rSentence range. As of the end of the core macro, the range is currently empty at the location of the deleted original sentence. We just need to test the next character to validate whether it’s at the end of the paragraph or not.
Unfortunately, when there is no text spanned by a range, the “next” character is actually found using the “First” character of the empty Range. This is a little confusing since it’s not actually spanning any text. I wish it wasn’t like that for consistency, but that’s why you have me here.
Given that oddity, our comparison needs to be whether the First character is a paragraph mark.
Delete the extra spaces
If we have a paragraph mark on the right, then select all spaces to the left and delete them. We’ve used If conditions before, but briefly they look like
The extra steps inside are only done if SomeCondition is true. For our condition, we use
The MoveStartWhile command works just like MoveEndWhile except it changes the Start position of the range.
Usually there is only one space to the left, but we might as well handle the general case using the MoveStartWhile command to extend over all spaces prior to the rSentence range.
We then delete any spaces found using the Delete method.
But we have a problem …
Unfortunately, the above will still delete a character if it detects a paragraph mark regardless of whether any spaces were found.
In addition to checking whether the character after the empty rSentence range is a paragraph mark, we need to make sure at least one space exists before deleting anything.
Checking two conditions
How do we check two conditions?
If we want both conditions to be true, we use an And between them.
With And between them, both ConditionOne and ConditionTwo must be true before the extra steps inside are carried out.
Detecting any preceding spaces
We add a second check for whether a preceding space exists before selecting and deleting them. We need to get the preceding character for the check.
The Previous method returns the range of the previous Unit specified which is a character here. We didn’t add a Count:=1 option because that is the default value.
We included the Unit in parentheses because we needed to access the Text property of the previous character range.
We only delete the space if the paragraph mark and at least one prior space both exist.
Overcoming Word’s “helpful” features
Sometimes Word re-inserts deleted spaces. This can be convenient in everyday use, so you don’t have to manually insert a space when you paste text, for example, but it can be quite annoying when creating macros … do you hear it coming?
Since this is the end of the paragraph, we should be able to just delete the excess space(s) and be done, but in my testing, Word indeed reinserts a space right at the end of the paragraph.
Arghhh.
We need to add another step inside the end-of-paragraph checks to detect a reinserted trailing space and then delete it despite Word’s attempt to help us.
We redefine the first character for the empty sentence range, but this time we’re checking whether it’s a space or not, the one Word reinserted automatically.
A simple Delete command deletes the single space Word reinserted to the right of the empty range.
I understand the point of Word adjusting the spacing for us since those features are implemented for real-time use of Word, but it does make our job harder at times when creating macros.
Wouldn’t it be simpler if …
Why not just delete the space every time? You might just add the extra delete either way.
After all, you “know” it’s there, right?
Uhhh … that generally scares me. Do we really know it’s there? Can I guarantee that Word will always insert it?
Maybe … but I don’t know.
I prefer to make sure the space is present first. Otherwise, I might end up deleting some other character. Probably not a big deal since it’s just one character, but I feel better about my macro if I check first.
Finally delete the spaces
Here is the result for deleting any excess end of the paragraph spaces.
Notice how this simple task of deleting excess spaces grew into a chunk of macro steps.
Reinsert missing spaces
When moving the last sentence of a paragraph, the copied sentence at the new location will not include an ending space because the original sentence ends with a paragraph mark instead. We omitted the paragraph mark on purpose from the initial sentence range, but we need to add a space after we move it earlier in the paragraph.
Now we’re working with the rCopy range since it is at the target location for the copied text.
Getting the last character
As a reminder, we’re doing this check right after extending the end of the rCopy range over any trailing spaces.
So, the range should include any spaces if they are present.
We need to check whether a space exists before adding one, so we get the last character of the range.
Inserting the space
Check whether the last character is already a space before adding one.
Remember <> is the “not equal to” symbol (literally less than or greater than), so we’re checking whether the LastCharacter is not a space before adding one.
The InsertAfter command does what it says. It adds the given Text option text within the double quotes to the end of the range, extending the Range in the process.
InsertAfter only adds the plain text given, but we can also incorporate special characters, if necessary. The inserted text will inherit any paragraph formatting or character formatting of the character immediately prior to the insert location if no spaces separate the text.
But not at the end of a sentence
Since we’re about to allow moving a sentence to the end of a paragraph (see more intuitive behavior next), let’s fix a small gotcha immediately.
We just added a space between two sentences above, but don’t want to add a space when it’s the last sentence of the new paragraph, so we need to exclude that case.
Ughh … we need the next character now.
The Next method works like the Previous method except it gives the next character after a range. There is a gotcha if the range is empty which was mentioned earlier, but here we know the range spans some text since we just copied it to the target location.
We don’t 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. The condition is the next character is not a paragraph mark.
So, we’re making two comparisons.
When using And, both conditions must be true before we add the extra space inside the If statement.
Spacing sentences properly
The resulting chunk of steps to ensure proper sentence spacing is
To get everything done correctly, the steps even for a small task seem to grow more than we might initially expect.
More intuitive behavior
Now we adapt the base macro to make it act more intuitively.
Moving the first sentence of a paragraph
The original macro will move the first sentence of a paragraph two sentences deep into the previous paragraph. This behavior is a little jarring in my opinion.
Instead, it should just become the last sentence of the previous paragraph. I think that makes the movement “flow” better.
Detecting the first sentence of a paragraph
To check whether we are at the first sentence of a paragraph, we need the previous character using the Previous method.
For a general sentence, the previous character can be any text, but if it is the first sentence of a paragraph, then the previous character will be the paragraph mark of the prior paragraph (unless it is the first sentence of the entire document which we’ll ignore).
Moving to end of previous paragraph
Rather than move two sentences backward in the document, which is the standard case, we just need to pick a different target location for the copied sentence.
In this case, we only want to move back to the end of the previous paragraph which is literally one character backward in the document. We use the rCopy range since we’re using it to mark the target location for the moved sentence.
We move by a Unit of character, and Count is negative 1 because we’re moving backward in the document.
The Collapse command isn’t strictly necessary since the move is made from the beginning of the range, but it makes the steps clearer.
Selecting the correct position
In the base macro, we always moved back two sentences, but now we have a different possible target location. We need to pick between this new end of paragraph position or the regular move backward by 2 sentences in the document.
We’re already detecting the beginning of the starting paragraph, so use the same condition to determine the target location.
If Else conditions …
We’ve used If statements several times, but here we have a regular case and a special case. We want to include both, so we use an If Else statement.
The first part is when we’re moving the first sentence of a paragraph, so we compare the previous character to a paragraph mark, and the second is for all other starting sentence locations.
Correct missing sentence space
The end of the previous paragraph probably doesn’t have a space to properly separate the sentences. Let’s fix that immediately rather than add a whole chunk of steps later. The rCopy Range is already at the desired location, so just add the space now.
If you really wanted, you could add a check for a space first. I would in my own version, but this handles the most probable case.
The InsertBefore command acts just like InsertAfter except it inserts the given text before the range as its name suggests.
Don’t overwrite the space
Unfortunately, the various Insert methods automatically extend the range over the inserted characters, so the space will be part of the rCopy range. This means our new space would be overwritten when we copy the formatted text only a few steps later. We can avoid this by just collapsing the rCopy range.
We collapse toward the end using the Direction option, so the space is before the upcoming copied text.
More intuitive target sentence position
Accumulating the steps to handle proper target location for the copied sentence text, we have
Now our macro will handle the regular case, but it will also handle backward moves more naturally for the first sentence of a paragraph.
Delete extra spaces at end of target paragraph
Unfortunately, we just introduced a small gotcha regarding proper spacing between sentences.
Huh? How?
Now we allow a sentence to move to the end of the previous paragraph. The moved sentence probably has a trailing space that moves with it, so we need to delete it like we did for the source paragraph earlier.
Ahhhhh! Get me out of here!
This is a good example of how changes to one part of a macro affect other parts. What do we do besides pull our hair out and give up on macros entirely (that’s not very productive)?
Reuse prior solution
Fortunately, we can reuse the previous steps where we deleted extra spaces at the end of the source paragraph. We’ll just need to change the range we used to correspond to the new copied sentence (target) range. We don’t want to disturb the overall copied text range, rCopy, since it holds our desired sentence content at the new location, but we do want to delete any excess spaces at the end if they exist.
Create sentence copy copy range
We just need to store a copy of the rCopy range in a new variable.
But we want to check for spaces at the end of the range, so we collapse it toward the End position using the Direction option.
Modified steps with copy copy range
Now we have an empty range just at the end of the copied text, so we can check for excess spaces and delete them, if needed.
This is a why functions exist
This is a big reason why functions exist. We literally just copied the same steps over and made a trivial change to a different range. Look for the upcoming article breaking this macro down into smaller bite-sized chunks.
Delete empty paragraph?
What if we moved the only sentence of our initial paragraph leaving it empty?
If you don’t care, then there’s nothing to do here, but it’s a little unsightly in my opinion to leave a blank line after the move, so let’s delete it.
We use the rSentence range to check whether the paragraph is empty since that range is still at the original deleted sentence location.
One way to check for an empty paragraph is to check for consecutive paragraph marks with no text in between them (since we’ve already deleted all text and any spaces). Given the range location, we’ll get the previous and next characters as we’ve done previously.
As a reminder, we need the First character of the range since we are sure (given our previous steps) the rSentence range is empty. If rSentence had contained any text, we would need the next character using the Next method.
Check whether both characters are paragraph marks.
A simple Delete command removes the lone paragraph mark.
Select moved sentence
This is another personal preference, but I like to end with the moved sentence selected as a visual clue that something significant changed in the document. We just select the copied text range using the Select method.
Plus, if the user wants, they can continue moving the same sentence.
Revised Macros
Let’s put these changes together into a revised, more intuitive macro.
Before you scan below
The special cases make them ungainly, but their complexity gives us a good reason to introduce functions and subroutines in upcoming articles. For example, there are several natural chunks of steps that can be neatly separated out and would probably apply to other macros later.
These macros are nevertheless fully functional as is, but the intention is to use them as springboards to create better macros in the future. However, 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 changes together (order matters), our current macro to move a sentence backward in the document is
Moving sentence forward
The move sentence forward version of the macro is also long. We need to adjust the corresponding steps (performed off screen) for moving forward in the document, so the details are a little different.
Too much
Ugh … they’re too long, and we haven’t even added in handling dialog and parentheses naturally.
You might notice some very similar chunks of steps within and between the two macros. Eliminating repetitive code is a main use of functions, but we’re already super long in this article, so that will be a lesson for another day.
Keyboard shortcuts
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 key combinations aren’t perfect in my opinion since I’d prefer those key combinations extend the selection by sentences instead (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 simply select them.
Differences ...
Some details or gotchas don’t pop up in both macros. For example, we don’t have to worry about the second-to-last sentence target position correction. It’s a subtle distinction, so when creating your own macros with slight variants like these, don’t assume everything is the same.
Comments
Notice the regular comments throughout both macros. They would be even more difficult to follow without the additional explanation.
You’ll thank yourself for including comments if you ever go back to the macro later. I’ve forgotten what I was thinking or how I was solving a problem at the time, and the comments helped me remember and saved me some time fixing or enhancing the macro.
I will generally explain notable individual steps, chunks of steps, what variables mean, or just what I was thinking at the time if it might matter later. I’ll also note any specific issues I was having such as why something didn’t work. If there is a strange step, I’ll add a note about why it was included.
Other improvements for later
With so many changes in sequence, we should probably add an Undo record and maybe even suppress screen updates during the macro since we repeatedly change the document on screen, but those are lessons for another day.