We create a pair of macros to move a word left or right in a document. The macros also accounts for dialog and parentheses and applies the correct capitalization when appropriate.
Thanks for your interest
This content is part of a paid plan.
Move a word in a document
Moving a word left or right in the document seems like a trivial task. On the road to streamlining editing tasks, we turn it into a keystroke while coercing VBA into handling most of the movement details like capitalization, being smart around dialog or parentheses, and correcting spacing after the move.

This is another macro I initially created with a twinge of guilt. Writing a macro is often easier, and more fun in a programmy kind of way, than writing fiction. Plus, I'm still being "productive" … right? It'll save me time down the road … yeah. Fortunately, I've discovered I use it often enough I'm glad it's available. I think you'll also be pleasantly surprised at how handy it is.
Create the empty macro
Open the VBA editor in Word using Alt+F11 in Windows (or Option+F11 on a Mac) empty macros.
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, and we have a lot of them to add in this macro.
What are the manual steps?
A word can move forward or backward in the document, so we need two variants. We'll focus on the backward movement for clarity and adapt it to move forward. In this set of macros, only minor differences exists between them.
What steps would we use to move the current sentence manually?
- Use the keyboard or mouse to select the current word
- Press Command+X on a Mac (or Control+X in Windows) to cut the word to the clipboard
- Tap Control+ or Command+Left arrow to move the cursor left one word
- Paste the word back into the document at the new location
- Adjust the capitalization if needed
- Correct any spacing issues if needed
The steps seem trivial for a word, but it occurs often enough that it's nice to have a quick shortcut for it.
Word often helps reasonably well with simple spacing corrections. We could also double click a word (see our mouse selections article for more tips) and then drag and drop the selection using the mouse. This is more powerful because we can drop it anywhere, but the mouse is generally slower than it feels.
Let the macro handle the details
An advantage of a macro is it can automatically correct capitalization, handle common punctuation, and correct any spacing issues. A fully fleshed out version could even detect lists and move the trailing comma with the word, but this extension is outside the scope of this article.
VBA techniques often differ from the manual steps
We usually don’t perform the macro steps exactly like we would do them in Word. Instead we use the various VBA object properties and methods which store and manipulate the document content directly rather than through a keyboard and screen.
Don't use the clipboard unless necessary
The above manual steps use the clipboard, but generally speaking, we should avoid changing the clipboard unless its directly related to the purpose of the macro. Using the clipboard may feel natural when writing a macro since we work with it manually. It also automatically preserves any text formatting. This latter benefit is important since the various insert methods and even the Text property all insert plain text, but VBA offers a better approach.
Why avoid using the clipboard in a macro?
The clipboard contents should not mysteriously change in the background while we're working in a document. When in the zone, we'll use the tools that are familiar to our fingertips without thinking deeply about side effects.
Suppose we're in the throes of editing our newest novel. We cut a paragraph but then notice a word is out of place. We quickly tap the shortcut for our new macro and move the word over. Then we jump somewhere else to paste the cut paragraph … but it's the word we moved not the cut text.
Ughhh. Somebody wrote a bad macro.
It's not a total loss since Word includes a clipboard history, but that's even more clicks when our macro is just supposed to work without causing any problems. The clipboard should only change when the writer expects it to change. For example, a CutParagraph macro obviously changes the clipboard contents based on its name, but a MoveWordLeft macro does not.
What's the macro plan?
Thinking about how VBA represents the document elements, we can sketch out a plan.
- Use two Range variables
- One will work with the original word
- A second target range will mark the move location
- Begin with the initial Selection range since it represents the current editing location in the document.
- Expand the working range over the current word
- Use the second range to find the move location
- Copy the formatted text to the target location
- Delete the original text to finish the move
- Correct any spacing issues at both locations
- Correct any capitalization issues if we move a word to or from the beginning of a sentence
Of course, other command variations would work, but the above is a logical process to accomplish the task in the context of VBA. Identifying the most intuitive word and locating the target position are both trickier actions than they seem at first glance. Catching proper nouns when correcting for capitalization is more involved than this article can cover.
Common conditions
Some common conditions pop up when working through the decision logic for the macro.
- Is the range at the beginning of a sentence?
- Are the characters before and after the range punctuation marks, a paragraph mark, or a space?
Rather than rehash them each time they arise, let's cover them together. The specifics may be tweaked depending on the test or the range being checked.
Declare some variables (optional)
The logic will be clearer if we use some conveniently named variables to store the various elements. It's more common to declare variables just before they are used for the first time in the macro, but I prefer to get them out of the way in many article macros, so we can focus on the editing steps.
Conditional variables
We store the above conditions in several Boolean (True or False) variables.
Text variables
We store the individual characters we will need for the various comparisons in several conveniently named String variables.
Working and target ranges
The ranges marking the original and target document ranges are:
We call the original word range simply r, and the target range is rTarget. I usually precede range variables with an "r" for clarity.
Detect the beginning of a novel sentence
How do we detect the beginning of a sentence?
Since the macro accounts for dialog, we also want to allow sentences that begin inside double quotes even if it's preceded by a dialog tag in the proper sentence.
Detect a typical sentence start
We'll refer to the working range r for simplicity, but the same logic applies to the target range rTarget.
Get the Start of the working range
Every valid Range variable includes Start and End positions marking its extent in a document. We can reference its Start property.
If the range is empty, the Start and End positions are the same. An empty range will affect some of the steps below, but for now, we want to determine if the range begins a sentence. Knowing the Start position is sufficient.
Get the Start of the sentence range
The Sentences collection of the range stores all sentences fully or partially contained in the range.
We need the First sentence of the collection.
In the current macro, we collapse the range as the first step after assigning it based on the initial selection, so First is the only sentence in the collection. First returns the range of the indicated sentence, but we only need the Start position.
We want to know if this position is the same as the Start of our working range. We simply compare the two numbers with an equals = sign.
On its own line, this would be an assignment, but when it's used in a conditional statement (such as If-Then, While, Select, etc.), it will be evaluated as a Boolean result.
Does the working range begin at the sentence range?
The various conditional statements below will be easier to read if we store the result in a conveniently named variable.
The parentheses (…) are not required since VBA would evaluate the statement on the right as a Boolean value and then store the result in the variable on the left, but it helps us humans understand it better. Having two equals signs—one for the assignment and the second for the Boolean condition—just looks bad.
Detect the beginning of a dialog sentence
We want our macro to respect dialog. More specifically, we require a capitalized word if it follows a left double quote. A left double quote is a single character, so we can reference the Previous method of the working range to get that character.
The Previous method defaults to a single character range, so we can omit both the Unit and Count options. However, we need its Text for the following comparisons.
Condition for the beginning of a dialog sentence
If the previous character is a left double quote, it begins a dialog sentence. A simple comparison is:
The LeftDQ constant is defined in a separate article for all macros in a module. While not required, quote characters appear often enough in editing macros that defining clear, module-level constants is convenient. We could also use the plain text character "“" in double quotes, but this can be difficult to read in some contexts. We neglect a straight double quote to simplify the comparison logic. Word usually automatically converts straight double quotes to left or right double quotes, so this should work for the vast majority of documents.
We do not check a paragraph mark since it automatically indicates the beginning of a sentence. We also omit checking an open parenthesis or a left square bracket since these grouping symbols do not commonly begin sentence text. The simple left double quote detection also accounts for dialog that begins with a dialog tag. A few exceptions could exist such as word “introductions,” special meanings, or short-story titles; but these would much less common in novels.
Detect a novel sentence compound condition
The two beginning of sentence conditions are mutually exclusive, but either can be True and still count as the beginning of the current sentence. This corresponds to a Boolean Or operator.
The parentheses improve the clarity at the expense of making the assignment a tad messier. The underscore _ character at the end just continues the long line in the editor. VBA does not care as long as we don't exceed 25 editor lines for a single command line.
Punctuation mark comparisons
Some of our decision making logic for moving a word will hinge on what punctuation marks border our range. Occasionally, spaces or paragraph marks will need to be checked also. Let's assume we have a string variable sFirst that stores a single character.
Typical string comparisons with an equals = sign
A long time ago in computer years (but not in a galaxy far, far away), people that write programming languages (yeah, they actually exist) borrowed the equals = sign notation from mathematics for comparing text. Later languages adopted better variations like a double equals == sign to distinguish comparisons from assignments, but we're stuck with the equals sign for comparisons in VBA.
Using some variation of equals makes more sense when we realize characters are literally stored as numbers in a computer. All the variable stuff and double quotes around the plain text just hides it. Text comparisons must match exactly, in general, but we're comparing single characters to punctuation marks, a space, or other special characters. None of them are alphabetic letters, so the distinction is not important in this macro.
Simple character comparisons
Suppose we have a stored character in a variable named sFirst. We again use an equal = sign to compare the variable to the expected text.
The comma is a plain text character, so we need to put it in double quotes for the comparison. Special characters can also be used by referring to the Word constant or your own constants.
A paragraph mark is a special character which is defined in a miscellaneous Word constants table. LeftDQ is defined in a separate article for all macros in a module. We could alternatively compare sFirst to the literal left double quote character as plain text "“". We will use this character in places for brevity, but it's not as clear, in general. We literally have to copy it in from somewhere else since our keyboard typically types straight double quotes.
Compound text comparisons
We need to test against multiple punctuation marks. We could just test against each intended punctuation mark individually and chain them together with Or operators.
Or results in True if any of the conditions are True. We will include some direct character comparisons like this, but it's already getting messy, and we're not even done. We need a notation that will condense some of the comparisons.
Use Like for the character comparisons
The cleanest way to compare to many possible characters uses the Like search operator.
Huh?
That sounds like a search not a character comparison. Yeah, but it does double duty.
What does Like do?
Like does general plain text searches similar to what Find does in Word, but we can leverage Like to simplify our single-character comparisons. In fact, it's about five times faster than chaining together all the conditions. Like is not as common in programming circles, but it is convenient at times. We summarize several Like features that meet our current needs.
How does Like work?
The SearchText on the left side is the plain text to search. It can be any text, but it's just a single character in this macro. It's usually stored in a String variable. The search pattern can be more complicated, but for our comparisons, we just want to match any of several punctuation marks or a space.
What result does Like give us?
Like gives in a True or False (Boolean) result based on whether it found a match.
What is a search pattern?
Search patterns are plain text strings describing the text to match in the SearchText variable. The patterns can get complicated, in general, but ours is relatively simple. The search pattern is usually given as plain text inside straight double quotes, but it could also be stored in a String variable.
Using a character group in the search pattern
General patterns can often be difficult to read, but we're not getting complicated with it. If we want to match any of several characters, we use a character set in square brackets "[]". For example, a search pattern of "[abc]" will match and "a", "b", or "c" but not a "d" or "x".
Our search string is sFirst from above. The search pattern includes many sentence punctuation marks.
The square brackets indicate optional characters in what's called a "character set," and Like will attempt to match one of the characters inside the brackets. The brackets are not searched characters. They just mark which characters make up the character set in the search pattern.
No other characters are included in the search pattern, so Like expects nothing else to be present for a valid match on either side of the punctuation mark. More specifically, if any other character is present in the sFirst variable, before or after the punctuation mark, Like will say the search text doesn't match the pattern.
The double quote inside the string "”" is the plain text right double quote character. A separate article also defines it as a module-level constant RightDQ, but the above search pattern seems clear enough. This search pattern omits a straight double quote character for clarity. They're not as common in novels since Word usually automatically converts them to left or right double quotes. We further include an em-dash and an en-dash, which are copied over from Word, since they require minimal effort.
Issues with using Like?
On a more technical point, Like cannot match a right square bracket "]" because it is used to define the character set in the search pattern. Right square brackets "]" are not common in novels, but in the interest of generality without much extra effort required, we'll include it as a separate condition. A paragraph mark also won't work with a Like text search. A left square bracket can be used, but the notation is messy. We'll include separate conditions when we need these characters.
Like just gives us a yes or no answer about whether a match was found. This literal bit of information is sufficient in this macro. More generally, it will not tell us how many matches exist or where they occurred in the text. If the latter information is needed in another macro, the InStr(…) function is a little messier but still relatively simple to use. As a last resort, regex takes a sledgehammer approach to text searches (see our introduction to regex in VBA article), but it's not commonly used in Word macros.
Why not just use Find instead of Like?
The Find property in VBA allows us to search a Word document using a Range variable. Find is powerful and fast, but we're searching plain text stored in a String variable. They're similar tasks but not the same thing. Plus, Find is a bit of using a hammer to squash a fly (or a mosquito) since we just need to compare individual characters.
Leftward punctuation mark
When working through the punctuation logic, we need to do different subtasks depending on what punctuation is before or after the range. Often these come in the form of grouping symbols such as an open parenthesis "(", a left square bracket "[", a left double quote "“", or an em-dash "—". Straight double quotes are used with each to make them plain text strings.
Using a single character stored in a String variable sFirst, we create a search pattern using Like.
We won't consider every mark in every condition since it depends on the specific decision.
The above pattern does not include a left square bracket "[". Like can match a left square bracket, but it's messy, so we'll add the comparison separately.
In the context of text division, a paragraph mark also separates a word from other content, so we may need to consider it as well. It's often redundant information with the beginning of a sentence, so we could remove the extra condition, if some specific logic doesn't need it.
We could just use this statement in a conditional statement, but the compound conditions get messy and long, so we assign the result to a variable IsLeftMark.
Rightward punctuation mark
Like allows us to just add to or swap characters in the search pattern.
Like cannot search for a right square bracket or a paragraph mark in a character set, so we need to add those separately.
We again assign the Boolean result to a variable for later use in a conditional statement. Unfortunately, some punctuation marks—such as an em-dash—can appear on either side of a text group. When a logical conflict arises, we need to pick one for the decision logic.
Move a word backward
After setting up the preliminary logic, we can finally implement a macro to move a word backward by one in the document. Then we'll adapt it to move the other direction. Like the previous move sentences macros, this macro is deceptively tricky. It's not particularly hard to implement, but enough details lurk in the shadows that it's easy to get tripped up.
Assign the current document position
The Selection is the VBA object that stores and manipulates the current selection or insertion point in a document. While it is like a Range, it is more than one, so we refer to its Range property when assigning it to our initial working range r.
Any object assignment requires the Set keyword. While this assignment sets the ranges initially equal to each other, the two ranges are independent of each other. This is important since we're about to manipulate the original word range based on what text is present in the document. In the current macro, we'll reveal the changes before we finish.
Collapse the working range
We immediately collapse the working range using the Collapse method.
While it is convenient to omit this step and allow multi-word ranges, it ensures the later logic does not encounter any extra problems.
Expand the working range over the word
We now expand the working range r over the current word, so we can copy it to the target location later.
The Expand method uses the standard Word Unit constants table. Available units include the obvious ones in documents such as words, sentences, and paragraphs; but the default Unit is a word, so we can omit it.
The Expand method extends the range both directions to the full extent of the indicated unit but no farther. Since we collapsed the working range r above, this means we're expanding our range variable over the word at the beginning of the original document selection or insertion point.
Modify the word range
We want an intuitive word choice, but Word considers punctuation and even a paragraph mark to be separate "words," so we need to work around this assumption. I include most cases below for completeness, but feel free to skip ahead. Since we collapsed our working range, we'll consider only an empty starting range.
If the Expand method …
- Starts inside a word, it expands over that word (easy).
- Starts at the beginning of a word, it expands over that word (also easy).
- Starts at the end of a word, it expands over that word unless it borders a punctuation or paragraph mark.
- Starts in some whitespace at the beginning of a paragraph, it will extend over only the spaces (odd).
- Starts in some whitespace at the end of a sentence but not next to a paragraph mark, it will extend over previous word including the spaces.
- Starts adjacent to a paragraph mark, it will extend over the paragraph mark.
- Some other special cases may exist.
See all the cases?
Roughly speaking, it favors selecting the word immediately to the right of the insertion point position. On the other hand, if this word is actually a punctuation mark, a typical user probably intends to move the preceding adjacent word, so we need to make some adjustments.
It's not difficult, but we need to decide what "intuitive" means for our macro and be careful to implement it correctly. Our macro should work naturally and not frustrate us with any actions it takes. Unfortunately, the number of cases and all the variations with different punctuation marks—comma, semicolon, period, parenthesis, question mark, exclamation mark, and more—makes it a bit of a challenge to implement cleanly.
How can we do it without a stack of conditions?
Rough punctuation mark conditional statement
Working out the rough logic, we want to make a decision based on whether a punctuation mark is found. We'll start with some decision logic on a conceptual level, but some later conditional statements will just give the VBA.
These cases are preferences based on what seems intuitive. Just because we're using a computer, doesn't mean our macros have to act like it. They can save us a few brain cycles by making some reasonable and intuitive decisions based on the local document content.
Get the first character of the range
We need the first character of the working range r for the comparison. We use the First property of the Characters collection.
The First property returns the document range of the character, but we need the actual text character for the comparison below, so we refer to its Text property.
We stored it in a string variable sFirst for convenience.
Punctuation mark conditions
Using the various general text comparisons above, our decision hinges on whether a left- or right-side text group punctuation mark is present as the first character of the empty working range. Our conditions are:
A paragraph mark is an important special case since a writer might run the macro at the end of the current paragraph, but it's not used on the left side much because it's placement is redundant with the beginning of the current sentence. An em-dash could be on either side of a text group, so we just pick the right side. The extra parentheses around the triplet condition for IsLeftMark is just for extra clarity.
Reassign the working range to the previous word
If the range begins with a right-side punctuation mark (such as a close parenthesis that ends a text group), we probably want to move the previous word instead. We get it using the Previous method.
Previous returns the range of whatever unit occurs just before the range variable location in the document. The default is a character range, so we assign the word Unit constant wdWord to the Unit option since the default unit is a character.
We omit the Count option since it defaults to the first unit. Parentheses are required around the Unit option because we're assigning the result to a variable. We reuse the working range r because it is the intended range variable for the word to move.
Reassign the working range to the next word
Similarly, if the range begins with a left-side punctuation mark, we probably want the next word just inside the text group. The Next method does that.
This variation isn't as intuitive as the above case, but it makes enough sense to include it. Next works much like the Previous method, but Next returns the unit range of whatever after the working range.
Revised conditional statement
Combining the above pattern match using Like, this becomes:
The user probably doesn't want to move the punctuation mark, so the reassignments to the previous or next adjacent word make the macro work more intuitively. See our introduction to conditional statements in Word VBA for more explanation of the If-ElseIf-Then structure. Since we did all the heavy lifting with the earlier conditions, the conditional statement reads almost like English.
Assign the target range
We need to find the target location just before the previous word. We again use the Previous method, but this time, we assign the previous word range to the target range variable rTarget.
We will fine tune the target range location based on the nearby punctuation.
Adjust the target range
We want to skip past most punctuation marks to find some regular alphanumeric text. The relevant method is MoveEndWhile.
MoveEndWhile literally moves the End of the range "while" it keeps finding any of the characters listed in the character set. We assign the punctuation marks to a separate convenient string variable and then assign that variable to the Cset option. We need to move the range backward in the document, so we also assign the wdBackward constant to the Count option.
After this command, we're somewhat sure our target range spans or is positioned next to the first alphanumeric word before the initial target range. This position can also move backward into a prior paragraph. We'll correct any word spacing issues toward the end of the macro.
Collapse the target range
We want to move the current word in the document before the previous one, so we Collapse the range.
The Collapse method defaults to collapsing toward the start of the range. This is what we want, so we can omit the Direction option.
Why move the End position not the entire range?
Why move the End position and then Collapse it afterward? That is, why not just move the whole range with MoveWhile instead?
The effect is subtle in this macro. MoveWhile would automatically collapse the target range toward the start and move past any punctuation marks to the previous word. Unfortunately, it gets the wrong position if the initial target range is at the beginning of a sentence. Oddly, it gets the awkward cases correct, but we need our command to handle all of them.
MoveEndWhile works similarly, but it doesn't skip over the initial word if it's a regular word. It just skips over irrelevant punctuation or whitespace. We then manually collapse the target range to properly position it for the moved word. Other logic would work, but MoveEndWhile provides a nice, clean solution.
Copy the original text to the target location
We now copy the original word text to the target location. We want to keep any text formatting, so we use the FormattedText property. We can just set the two properties equal to each other.
The Text property or one of the Insert methods would only copy plain text. If we had not collapsed the target range, this assignment would replace any text in the range (akin to pasting text into a document when a selection already exists). This step is why we needed two ranges. We needed to both know the original word and identify the target location for the copy step.
Delete the original word text
We want to move the word not copy it, so we should Delete the original version spanned by the working range r.
This deletes all content spanned by the working range much like tapping the Delete key when a selection exists in a document. However, we can save a step if we first check for and include a comma following an introductory word.
Detect an introductory word
The original word may sometimes be followed by a comma at the beginning of a sentence. Tweaks such as this one make the macro a tad more useful since it reduces how often we need to clean up after the macro.
However, a comma can appear elsewhere in a sentence where we don't want to delete it. Is it a comma after the last word of a non-essential phrase, a first word in a list, or something else. Writing a macro to handle the general case is challenging since we kind of need to interpret English. However, if the word begins a sentence, it is almost certainly an introductory word, and we can handle that easily.
Is the working range at the beginning of the sentence?
We already mentioned how to check if a range begins a sentence, so the condition is:
The sPrevious variable just adds a little extra clarity.
Does a comma follow the working range?
We need to know whether the character immediately after it is a comma. We use the Next method.
We stored the next character in the sNext variable just to be a tad clearer. The default unit for Next is a character, and the default count is one unit, so we can omit both options.
If the range spans an introductory word, this next character will be a comma ",". We just compare them as text.
This is a True or False condition for a conditional statement, not an assignment.
Compound condition to catch an introductory phrase
Putting the two conditions together, we have.
Both conditions must be True before we include the comma in the working range, so we need an And operator.
Include the comma in the working range
If we have an introductory word, we also want to delete the comma. This is easiest if we just extend the working range over the comma using the MoveEnd method. A comma is counted as a Word, so we extend the End position of the range forward by one word.
MoveEnd moves the End position of the range by a specified unit. The default Count is one unit, so we can omit it. Using a word, rather than a character, unit automatically includes any trailing spaces. This helps reduce the chances of any spacing mistakes.
Delete the comma conditional statement
Putting the conditions together into an If statement, we have:
Catching Word's pesky help
Unfortunately, when we delete a range, Word will sometimes "help" us and reinsert a space. This occurs mostly annoying at the end of a paragraph or before a grouping symbol. Moreover, Word only adjusts the spacing after the range is deleted, so we must catch and correct for it after the above Delete method.
If Word reinserts the space, it will be immediately to the right of the working range r. For an empty range, this is actually the "first" character.
If this character is a space, we want to delete it. The condition is:
We can again use the Delete method, but this time, it will delete the character to the right of the empty range much like tapping the Delete key when no selection exists in a document. We can use the condensed form of an If statement.
Correct word spacing
After the word is moved, we need to correct any spacing issues at both the original and target positions. We could try to work the corrections into the above movement logic, making the changes as soon as they are necessary, but it's simpler if we just make the adjustments after the dust settles.
What corrections are necessary?
- Delete a space before either range if two spaces are present
- Delete a space after either range if it's next to a punctuation or paragraph mark.
- Add a space between words on either side if one is missing
- Do not add a space before a punctuation mark or a paragraph mark
The corrections are made after the move, but they still take advantage of specifics pertaining to the movement steps. A separate function article does this more generally with an arbitrary starting range. While it's not as efficient, it's handy to just offload the whole subtask to a separate function.
Get the previous and first characters for comparison
We need the nearby characters for comparison which we can get using the Previous and First methods.
We again store the characters to make the steps easier to read.
Detect the previous punctuation
As part of the comparison, we need to check the characters on either side of the working range. We previously worked through the details, so the conditions to detect left or right-side punctuation marks are:
A paragraph mark is a special character vbCr which is defined in a standard Word constants table. LeftDQ is assigned as a module level constant in a separate article. The plain text punctuation marks must be given in straight double quotes to indicate single character strings. A right square bracket and a paragraph mark must be tested separately since Like can't include them in a character sets for the search patterns.
Delete an extra space on the left side
As part of subtask 1 above, we delete a prior extra space from the document.
Detect a prior space with a right punctuation mark
We delete a prior space if the working range is just before a punctuation mark. The compound condition is:
And is required because both conditions must be True before we delete the space.
Delete the extra space
If we found an extra space, we delete it. The Previous method returns the range of the previous character.
Then we refer to the Delete method for that character range.
Previous allowed us to manipulate the document content outside the range.
Delete an extra space on the right side?
Similarly, we would need to delete a space after the empty range if it's followed by a right-side punctuation mark. We do not need to do so based on the prior delete steps. The working range automatically included any trailing space(s), so we can omit this case.
Insert a missing space at the working range
On to subtask 3 above for the working range. Is a space missing?
Detect a missing space at the working range
We add a space before the empty working range if it is missing but not if it appears at the beginning of a paragraph, a left grouping symbol, or a left double quote. Unfortunately, em-dashes can appear on both sides, so a bit of ambiguity exists.
We need to insert a space before the working range if it is missing. More specifically, if the previous character is a not already a space, we insert one. We compare the previous character variable to a literal space character " ".
The not equals <> symbol is the opposite of the equals = symbol. For text, not equals gives a True result if the two strings are not exactly the same.
We only want to correct the spacing if the working range is not preceded or followed by a paragraph or a respective punctuation mark, so we restrict it based on the previously stored IsLeftMark and IsRightMark conditions.
Switching to a Not operator makes the compound condition easier to read.
The rephrasing is based on some basic Boolean logic outside the scope of this article, but work through the various cases if you're not convinced the two expressions give the same results. The parentheses are required on the second expression because Not must apply to the result of the compound condition.
Insert the missing space after the working range
If the target range is missing a prior space, we add it with the InsertAfter method.
It requires some text to insert, so we assign a single space " " to the Text option. Since the working range is empty, the InsertBefore method would do the same thing.
Correct working range spacing issues conditional statement
Putting the spacing correction steps together for the working range, we have:
The conditions are stacked because they are roughly successive checks.
Correct the spacing before the target range
Onto subtask 1 from above but for the target range, we want to delete a space if it is followed by one of several punctuation marks.
Get the previous character
We again need the previous character for the upcoming comparisons except we reference the target range.
Is the previous character a space?
We've done this several times, so what is the condition for whether the previous character is a space?
Once you get used to seeing the VBA code, it's just as easy to omit the variable assignment and reference the previous character text here in the condition, but this is clearer for a long article.
Is the previous character a punctuation or paragraph mark?
We need to check whether the previous character is a punctuation or a paragraph mark. We covered the compound condition above, but we make some changes for the target range.
Insert and trim a missing space before the target range
We need to insert a space before the target range using the InsertBefore method.
The InsertAfter method would not work in this case because the target range spans some text.
We will select the target range just before the macro finishes. Most automatic Word selections do not include any prior spaces. Following that typical behavior, we trim the new space from the range using the MoveStart method.
We're just trimming it from the beginning of the range, not deleting it from the document.
Insert a missing space before the target range conditional statement
Putting the conditions and the character steps together, the conditional statement is:
Correct the spacing after the target range
For subtask 2 from above for the target range, we need the next and last characters at the end of the range.
Get the next character
We get the character after the range using the Next method with the target range variable. The Text property again gives us the plain text character which we then store in a convenient String variable.
Unfortunately, the intuitive next character depends on whether the range spans any content or not, but we know we just moved the word to the target range. That is, we know it contains some text, so Next works correctly here.
Get the last character of the target range
If a space is present, it will be the last character of the range. We store the character in a String variable.
Technically, more than one space could be spanned at the end of the target range, but we omit the general case for brevity—
[Cough. Reader spits his or her coffee across the screen.]
What?
Yeah, do you want this article even longer?
Detect a trailing punctuation mark
Leveraging the previous punctuation mark detections for the target range, the Like search operator again detects a punctuation mark on the right side of the range. The search pattern includes most punctuation marks.
Detect and extra space on the right side
If the last character is a space followed by a punctuation or paragraph mark, we need to delete the space. What does that look like in VBA?
Is the last character a space?
We've seen similar conditions several times.
Is the next character a space or a punctuation or paragraph mark?
Combining the space and punctuation mark conditions, we have a compound condition.
The parentheses just make it easier to read when we put it together. Or is necessary since either condition being True means we need to delete the space.
Do we delete an extra target range space?
Nope, we're not done yet. Your coffee refill can wait. If the above condition is True and the last character is a space, then we Delete it.
It's a more complicated condition because a space must be present before we delete it, but the delete trigger can be either an extra space or a right-side punctuation mark or a paragraph mark.
Delete the last space of the target range
If we want to delete the last space. We again need the range of the Last character.
We refer to the Delete method for that character range (not the target range).
As a reminder, this deletes the character from the document, not just the range. You can get your coffee refill now if you want.
Insert a missing space after the target range
If no space is present after the target range, we need to add one. More specifically, if both the last and next characters are not spaces and a right-side punctuation mark is absent, then insert a space.
Huh?
Let's dig through it.
Detect a missing space after the target range
The basic logic is the same as above, but we need both of them to not be spaces.
Even if both characters are not spaces, we still don't insert a space if the next character is a punctuation mark or a paragraph mark. All three conditions must be True before we insert a space.
That's difficult to read, but we can rewrite it using the Not operator, which may be a little cleaner.
This is logically the same as the former. I prefer the second one, but it does look more like programming. The parentheses are required because the Not operator applies to the result of the character checks.
Insert a missing space after the target range
We add a space using the InsertAfter method.
It requires a Text option, and we want to insert a single space " ". The space is automatically included in the new target range, but we want this since it mimics the typical Word behavior for automatic selections.
Conditional statement to insert a missing space after the target range
We can accomplish the above corrections with an If-ElseIf conditional statement (see our introduction to conditional statements in VBA for more explanation). Putting the conditions and the command together, we have:
Correct any word capitalization issues
We need to check for any capitalization issues for both the working and target ranges since we could move a word to or from the beginning of a sentence. The basic logic for correcting any capitalization inconsistencies would be:
Correct the working range capitalization
We'll begin with the working range.
Get the previous character
Correct any capitalization issues around the working range. We again need the previous character to detect whether the word begins a dialog sentence.
Detect whether the working range is sentence start
Capitalization is dependent on the placement in the sentence, so we need to determine if the working range is there. We previously covered the condition above, so the variation for the working range is:
The working range may have changed, so we reevaluate the compound condition to be sure.
Correct working range word capitalization
Range variables have a Case property that allows us to change the capitalization. While the Case property stores a value corresponding to various capitalization states, it does not work like many other properties. Think of it more like a dial we can twist to change the current capitalization across the range even if it's currently in a muddled state.
In this macro, our empty working range is placed at a single word or punctuation mark. We assign the capitalization constant wdTitleWord to its Case property.
If the range is empty, it changes the capitalization of the current word to its right side. Our macro takes advantage of this feature. We will not consider multiple words in this macro. This assignment has no effect on other characters like punctuation marks, so we don't need any extra conditions or tests.
Cannot use the sentence title case constant
The wdTitleSentence constant seems more intuitive for the subtask, and it would work for a typical sentence start. Unfortunately for a dialog sentence, the Case logic would detect that the word beginning the dialog is not at the proper beginning of the sentence, and it would not capitalize the word as desired.
Correct target range word capitalization
If the working range is at the beginning of the sentence, the target range cannot be, so we want to undo its capitalization.
We assigned the wdLowerCase constant to the Case property since we want to undo the standard beginning of sentence capitalization the target range word had before the move.
Working range capitalization conditional statement
Putting the commands together, we get:
Correct target range capitalization
We need a similar capitalization correction if the target range happens to finish at the beginning of the sentence. The logic is very similar to that above for the working range.
Get the previous character
We again need the previous character but for the target range.
Detect whether the target range starts a sentence
Capitalization is dependent on the placement in the sentence, so we need to determine whether the target range is positioned there. We previously covered the condition, so the variation for the target range is:
Correct the target range capitalization
If the target range is positioned at the beginning of a sentence, we capitalize the target range word using the Case property.
This works as expected because the target range only spans a single word. We would need to do something different if the range spanned more content such as a whole sentence.
Correct the next word capitalization
The following word was the beginning of the sentence, so we make it lowercase. We refer to it using the Next method.
The default unit is a character, so we need to specify a word unit.
We assigned the wdWord constant to the Unit option. The default Count is 1, so we can omit it here. The parentheses are required because we'll refer to a property of the word range.
Now that we have the range of the next word, we can again use the Case property to make it lowercase.
We assigned the wdLowerCase constant to the Case property.
Conditional statement to correct target range word capitalizations
We use the same basic If statement structure, but we insert the target range capitalization steps.
See the above comments on capitalization issues when using the Case property. Doing the spacing corrections before the capitalization corrections ensures the next word is indeed a separate word.
Select the moved word (optional)
I prefer to finish the macro with the moved content selected as a visual indicator of the change. The target range already spans the moved word, so we just use the Select method.
This command literally selects the range contents in the document.
Move a word right
The spacing and capitalization corrections were general (but not exhaustive) steps, so modifying the macro to move the current word right in the document is not overly complicated.
Any changes to the working range?
Setting up the working range to span the most intuitive current word in the document works the same, so no changes are necessary.
Changing the target range
Of course, the target range for the moved word must accommodate the new direction.
Identify the next word after the working range
We use the Next method to get the next word in the document.
Move past some punctuation marks
Word considers individual punctuation marks to be separate words, so we want to naturally move the original word past any punctuation marks. We use the MoveStartWhile method.
MoveStartWhile moves the Start position of the range past any characters given in the character set option Cset. We again use a simplifying string variable sMarks for the assignment. The default movement is by any number of characters forward in the document, so we can omit the Count option. See the commentary above about some of the characters are included and why we use the MoveStartWhile method instead of the MoveWhile method.
Collapse the target range toward its end
We want to move the current word after the next one, so we collapse the range toward the end.
To collapse toward the end of the range, we assign the wdCollapseEnd constant to the Direction option.
Gotchas
When a macro is doing as many things as this one, it may be intimidating to think through any trouble spots. Skip to the final macros below if you're not interested in the details.
Problems with capitalization corrections
Both assignments can encounter capitalization problems since they naively follow the capitalization rules. They just apply the indicated rule without respect for whether the word is a proper noun like Harry or an unusual word like FirstParagraph. The former would still be made lowercase with wdLowerCase, and the latter would lose the capitalization of the "P" even when it is capitalized. Neither of these are desirable changes.
How can we catch proper nouns?
Well … it's really a bigger topic than a gotcha section can solve, but let's talk about it. What are some options?
- A quick and dirty solution might define a few common proper nouns for the work in progress such as Harry, Monica, Denver, etc. The logic could then check against these few words with minor changes to the macro.
- A plain Array storing the words would work, but VBA does not allow us to define global constant arrays (if that means anything to you). It would be messy to look up a word, and it would also be slow if the number of stored words gets large.
- If we try to use a fancier data structures that include a convenient way to lookup a word, we need to enable some extra tools.
- Using a custom Word dictionary doesn't quite work either since the CheckSpelling method automatically refers to the standard dictionary in addition to any custom dictionaries specified.
- We could create our own proper noun dictionary-ish class and declare a global variable … but yuck. That's a lot of work and general overkill for the task.
Various workarounds exist, but each is clunky in its own unloveable way.
Sorry, VBA.
What's the problem?
If we try to use a custom Word dictionary, we're stymied by the fact that Word automatically checks the main dictionary by default … but we can't prevent it from doing so. Any regular word will be validated not just proper nouns. A logical workaround would check the word twice with and without a custom proper nouns dictionary. It's not a horrible approach, but it's awkward and requires an external proper nouns dictionary file stored somewhere. File access can be clunky in Word VBA, and Word for Mac can make it even clunkier.
Macros leave no final state after they finish running (mostly, it gets complicated), so a global list or array must be reassigned every time word launches. A workaround would use the AutoExec macro and a module-level array-like variable. It's probably the best, simple-ish solution, but it's still not fun.
It's just a mess no matter how we do it. Some solutions are better than others, but it ends up being more complicated than it should be unless we're hacking together a quick, personal solution.
Watch out for deleted text
Anytime we delete content, we should think harder about potential problems. Fortunately, we just move a single word nearby in the document, so the most likely problem is placing it somewhere unexpected. We could just undo that.
What about weird starting content?
If the writer runs the macro in something like an empty paragraph, the macro can do something unexpected. Fortunately, several of the repositioning or reassignment steps try to circumvent any issues and keep the results reasonable.
What if an initial selection exists?
A typical gotcha consideration for most editing macros is whether an initial selection exists or not. If so, what do we do with it?
No problem exists in this macro because we immediately collapse the working range after the Selection assignment. This is a relatively common first step to just avoid any issues with an initial selection.
The Collapse method does not affect an insertion point, so the macro always has a known starting condition. When we're unsure about how a starting selection will affect our macro, and it isn’t necessary for the macro to function properly, it’s not a bad idea to just collapse the Selection (or Range) as an early step in the macro.
Final move word macros
Put the commands together, and our lengthy move word macro is:
The move right version is very similar, mostly changing few move commands. We do, however, need separate macros since Word doesn’t allow parameters (in the parentheses) when assigning macros to keyboard shortcuts.
I assigned my versions of these macros to Command+Option+Left or Right arrow in Word for Mac or Control+Alt+Left or Right on Windows. These shortcuts are more of a compromise since moving words is handy, it doesn't occur as often as some other commands.
Improvements
What could we do better?
Catch proper nouns with the capitalization corrections
This is the most annoying shortcoming of these macros. As much as I talk about macros taking care of details while it performs a task, the above macros may mess up proper noun capitalization. Unfortunately, the best solution involves creating a custom Word dictionary. It's not difficult, but it involves multiple significant steps outside of the macros before it will work, so it was omitted from the above macros.
Catch other uses of double quotes
If someone wants to use the above macros in non-fiction documents, the assumption that a left double quote automatically implies a new dialog sentence might be inaccurate. Including some logic to catch alternative uses would be interesting but more difficult, in general, since double quotes have multiple uses in general text.
Disable screen updates
Despite using range variables, we make multiple sequential changes to the text. In the vast majority of cases, it will happen so fast it will look nearly instantaneous, but we could bump it up a notch and just disable screen updates until the changes are finished.
Undo record
I would probably add an undo record, so the multiple changes are all undone as a single step. An undo record can cause trouble in not implemented properly, so be extra diligent to do so if you include it.
Account for lists
When using my version of this macro, I am frustrated when it does not correct for commas in serial lists. It will move the word and leave doubled commas. This is exactly how the macro is designed to work, but it's annoying in a list context. The extension is beyond the scope of this article, but this would be my second improvement.
Use functions?
We do several things multiple times between the two macros. When we encounter a sequence of steps that do the same thing over and over in different macros, those are a good candidates to extract into a separate function. The main macro often seems simpler, and if we need that subtask in another macro, we just use the function.