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 word

Word • Macros • Editing
Peter Ronhovde
70
min read

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.

Example macro to move a move in a sentence

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.

Sub MoveWordLeft()
' Move the current word before the previous word

End Sub
Sub MoveWordRight()
' Move the current word after the next word

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, 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?

  1. Use the keyboard or mouse to select the current word
  2. Press Command+X on a Mac (or Control+X in Windows) to cut the word to the clipboard
  3. Tap Control+ or Command+Left arrow to move the cursor left one word
  4. Paste the word back into the document at the new location
  5. Adjust the capitalization if needed
  6. 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.

' Declare some conditional variables (optional)
Dim IsSentenceStart As Boolean
Dim IsLeftMark As Boolean, IsRightMark As Boolean
Text variables

We store the individual characters we will need for the various comparisons in several conveniently named String variables.

' Declare several string variables (optional)
' Each variable will store a single character for comparison
Dim sFirst As String, sLast As String
Dim sPrevious As String, sNext As String
Working and target ranges

The ranges marking the original and target document ranges are:

' Declare the working ranges (optional)
Dim r As Range, rTarget As Range

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.

' Get the beginning of the working range r
r.Start

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.

r.Sentences ' Not done ...

We need the First sentence of the collection.

r.Sentences.First ' Still not done ...

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.

' Get the start of the first sentence of the working range
r.Sentences.First.Start

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.

' Does the range begin a sentence?
r.Start = r.Sentences.First.Start

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.

' Condition to detect whether the working range begins a sentence
IsSentenceStart = (r.Start = r.Sentences.First.Start)

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.

' Get the previous character range relative to the working range
r.Previous ' Not done ...

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.

' Get the text of the character just before the working range
r.Previous.Text
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:

' Is the previous character a left double quote?
r.Previous.Text = LeftDQ

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.

' Compound condition to detect whether the working range begins
' a regular sentence or a dialog sentence at a left double quote
IsSentenceStart = (r.Previous.Start = r.Sentences.First.Start) Or _
(r.Previous.Text = LeftDQ)

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.

' Example of a typical plain text character comparison
sFirst = "," ' Is the character a comma?

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.

' Examples of some special character comparisons
sFirst = vbCr ' Is the character a paragraph mark?
sFirst = LeftDQ ' Is the character a left double quote?
sFirst = "“" ' Is the character a left double quote (variation)?

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.

' Compare the first character of the range to each punctuation
' mark (not used)
sFirst = "," Or sFirst = "." Or sFirst = "?" Or ' ... and more

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.

SearchText Like SearchPattern

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.

' Does the sFirst variable store any of the punctuation marks?
sFirst Like "[,;:.?!—”–)]"

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.

' Use Like to search for several left-side punctuation marks
sFirst Like "[(“—]" ' Not done ...

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.

' Use Like to search for several punctuation marks or a paragraph mark
IsLeftMark = sFirst Like "[(“—]" Or sFirst = "[" _
sFirst = vbCr

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.

' Use Like to search for several right-side punctuation marks
sFirst Like "[,;:.?!—”–)]" ' Not done ...

Like cannot search for a right square bracket or a paragraph mark in a character set, so we need to add those separately.

' Use Like to search for several right-side punctuation marks
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr

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.

' Set the initial working range to the current document position
' or selection
Set r = Selection.Range

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.

r.Collapse ' Avoid any selection issues (optional)

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.

' Extend the range both directions to include the current word
r.Expand

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.

If a right-side punctuation mark is found then
' Change the working range to the previous word ...
Otherwise if a left-side punctuation mark is found then
' Change the working range to the next word just inside the
' text group ...
End checking for a punctuation mark

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.

' Get the first character of the working range
r.Characters.First ' Not done ...

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.

' Get the first character text
sFirst = r.Characters.First.Text

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:

' Get a more intuitive word range based on the first character
sFirst = r.Characters.First.Text
' Does the empty working range precede some rightward or leftward
' punctuation characters?
' Some marks can be on either side, so pick one.
IsLeftMark = (sFirst = LeftDQ Or sFirst = "(" Or sFirst = "[")
' Like cannot match a right square bracket or paragraph mark
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr

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.

' Get the previous ... unit before the working range
r.Previous ' Not done ...

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.

' Change the working range to the previous word
Set r = r.Previous(Unit:=wdWord)

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.

' Change the working range to the next word
Set r = r.Next(Unit:=wdWord)

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:

' Adjust the working range to a more intuitive word choice
If IsRightMark Then
' Change the working range to the previous word
Set r = r.Previous(Unit:=wdWord)
ElseIf IsLeftMark Then
' Change the working range to the next word
Set r = r.Next(Unit:=wdWord)
End If

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.

' Assign the initial target range as the word before the
' working range
Set rTarget = r.Previous(Unit:=wdWord)

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.

' Move the end of the target range backward past any punctuation
' marks or whitespace
sMarks = ",;:.?!—“”–()[] " + vbCr + vbTab
rTarget.MoveEndWhile Cset:=sMarks, Count:=wdBackward

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.

' Position the empty target range before the previous word
rTarget.Collapse

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.

' Copy the formatted text to the target location
rTarget.FormattedText = r.FormattedText

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.

' Delete the original word (not done)
r.Delete ' What about a comma?

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:

' Does the working range begin a regular sentence or a dialog
' sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)

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.

sNext = r.Next.Text ' Get the next character

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.

' Is the next character a comma?
sNext = ","

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.

' Does the working range begin the current sentence range, and
' is it followed by a comma?
IsSentenceStart And sNext = ","

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.

r.MoveEnd Unit:=wdWord

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:

' Check whether a comma follows the working range and include it
sNext = r.Next.Text
If IsSentenceStart And sNext = "," Then
' Include the comma in the working range to delete it below
r.MoveEnd Unit:=wdWord
End If

' Delete the original word and maybe a trailing comma
r.Delete
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.

' Get the character immediately to the right of the empty range
sFirst = r.Characters.First.Text

If this character is a space, we want to delete it. The condition is:

' Is the first character a space?
sFirst = " "

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.

' Delete a trailing space if Word reinserted it
If sFirst = " " Then r.Delete

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?

  1. Delete a space before either range if two spaces are present
  2. Delete a space after either range if it's next to a punctuation or paragraph mark.
  3. Add a space between words on either side if one is missing
  4. 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.

sPrevious = r.Previous.Text ' Get the previous character
sFirst = r.Character.First.Text ' Get the first character

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:

' Does the empty working range precede some leftward or rightward
' punctuation characters?
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "[" Or _
sPrevious = vbCr
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr

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:

' Correct an extra space before the empty working range
sPrevious = " " And IsRightMark

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.

' Get the previous space range
r.Previous ' Not done ...

Then we refer to the Delete method for that character range.

' Delete the prior space
r.Previous.Delete

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 " ".

' Is the previous character not a space?
sPrevious <> " " ' Not done ...

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.

' Do we insert a space at the working range (not used)?
sPrevious <> " " And Not IsLeftMark And Not IsRightMark

Switching to a Not operator makes the compound condition easier to read.

' Do we insert a space at the working range?
' An equivalent expression which is slightly easier to read
Not (sPrevious = " " Or IsLeftMark Or IsRightMark)

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.

' Insert a missing space at the working range
r.InsertAfter Text:=" "

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:

' Correct any spacing issues around the empty working range
' We already know no spaces exist after the working range
If sPrevious = " " And IsRightMark Then
' Correct an extra space before the empty working range
r.Previous.Delete
ElseIf Not (sPrevious = " " Or IsLeftMark Or IsRightMark) Then
' Only thing left to check is a previous space
' Correct a missing space at the empty working range
r.InsertAfter Text:=" "
End If

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.

' Get the character before the target range
sPrevious = rTarget.Previous.Text
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?

' Is the previous character a space?
sPrevious = " "

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.

' Does the target range begin a paragraph or a text group?
' Left bracket character set match notation for Like is messy
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "]" Or _
sPrevious = vbCr
Insert and trim a missing space before the target range

We need to insert a space before the target range using the InsertBefore method.

' Insert a space before the target range
rTarget.InsertBefore Text:=" "

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.

' Trim the space from the target range
rTarget.MoveStart

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:

' Insert a missing prior space at the target range if needed
' No need to check for an extra prior space based on the moves above
If Not (sPrevious = " " Or IsLeftMark) Then
rTarget.InsertBefore Text:=" " ' Add the missing space
rTarget.MoveStart ' Trim the new space
End If

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.

' Get the next character after the target range
sNext = r.Next.Text

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.

' Get the last character of the target range
sLast = r.Characters.Last.Text

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.

' Does the empty working range precede some rightward punctuation
' marks or a paragraph mark?
IsRightMark = sNext Like "[,;:.?!—”–)]" Or sNext = "]" Or _
sNext = vbCr
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 last character of the target range a space?
sLast = " "
Is the next character a space or a punctuation or paragraph mark?

Combining the space and punctuation mark conditions, we have a compound condition.

' Is the next character of the target range a space, a paragraph mark,
' or a punctuation mark?
(sNext = " " Or IsRightMark)

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.

' Do we delete a space at the end of the target range?
sLast = " " And (sNext = " " Or IsRightMark)

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.

' Get the last character range of the target range
rTarget.Characters.Last

We refer to the Delete method for that character range (not the target range).

' Delete the last character of the target range
rTarget.Characters.Last.Delete

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.

' Detect whether a space is missing after the target range
sLast <> " " And sNext <> " " ' Not done ...

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.

' Detect whether a space is missing after the target range
sLast <> " " And sNext <> " " And Not IsRightMark

That's difficult to read, but we can rewrite it using the Not operator, which may be a little cleaner.

' Detect whether a space is missing after the target range
Not (sLast = " " Or sNext = " " Or IsRightMark)

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.

' Insert a missing space after the target range
rTarget.InsertAfter Text:=" "

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 spacing issues after the target range
If sLast = " " And (sNext = " " Or IsRightMark) Then
' Delete the unnecessary space at the end of the target range
r.Characters.Last.Delete
ElseIf Not (sLast = " " Or sNext = " " Or IsRightMark) Then
' Insert a missing space after the target range
rTarget.InsertAfter Text:=" "
End If

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:

If the range begins a regular or a dialog sentence then
' Capitalize the new word at the beginning of the sentence ...
' Uncapitalize the word moved away from the sentence start ...
End beginning of the sentence check

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.

' Get the text of the character before the working range
sPrevious = r.Previous.Text
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:

' Does the working range begin a regular sentence or a dialog
' sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)

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.

' Capitalize the word at the empty working range r
r.Case = wdTitleWord

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.

' Make the target range word lowercase
rTarget.Case = wdLowerCase

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:

' Check for and correct any capitalization at the working range
If IsSentenceStart Then
' Capitalize the word at the empty working range r
r.Case = wdTitleWord
' Make the target range word lowercase
rTarget.Case = wdLowerCase
End If
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.

' Get the text of the character before the target range
sPrevious = rTarget.Previous.Text
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:

' Does the target range begin a regular sentence or a dialog
' sentence at a left double quote?
sPrevious = rTarget.Previous.Text
IsSentenceStart = (rTarget.Start = rTarget.Sentences.First.Start) Or _
(sPrevious = LeftDQ)
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.

' Capitalize the word spanned by the target range
rTarget.Case = wdTitleWord

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.

rTarget.Next ' Not done ...

The default unit is a character, so we need to specify a word unit.

' Get the range of the word after the target range
rTarget.Next(Unit:=wdWord) ' Still not done ...

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.

' Make the word after the target range lowercase
rTarget.Next(Unit:=wdWord).Case = wdLowerCase

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.

' Check for and correct any capitalization at the target range
If IsSentenceStart Then
' Correct the target word capitalization
rTarget.Case = wdTitleWord
' Correct the capitalization of the word after the target range
rTarget.Next(Unit:=wdWord).Case = wdLowerCase
End If

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.

rTarget.Select ' End with moved word selected

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.

' Set the target range to the word after the working range
Set rTarget = r.Next(Unit:=wdWord)
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.

' Skip some punctuation marks for the upcoming word movement
sMarks = ",;:.?!—“”–()[] " + vbTab + vbCr
rTarget.MoveStartWhile Cset:=sMarks

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.

' Position the empty target range after the next word
rTarget.Collapse Direction:=wdCollapseEnd

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:

Sub MoveWordLeft()
' Move the current word backward one in the document while
' allowing grouping symbols and dialog
' Dialog is treated as a sentence start
' No capitalization checks are performed

' Declare several string variables (optional)
' Each variable will store a single character for comparison
Dim sFirst As String, sLast As String
Dim sPrevious As String, sNext As String
' Declare a string variable to store all relevant special characters
' or punctuation marks (optional)
Dim sMarks As String
' Declare some conditional variables (optional)
Dim IsSentenceStart As Boolean
Dim IsLeftMark As Boolean, IsRightMark As Boolean
' Declare the working and target ranges (optional)
Dim r As Range, rTarget As Range

' Set the working range to the current document selection
Set r = Selection.Range

' Expand the working range over the current word
r.Collapse ' Avoid any selection issues (optional)
r.Expand
' Adjust the working range to a more intuitive word range
sFirst = r.Characters.First.Text
' Does the empty working range precede some rightward or leftward
' punctuation characters?
' Some marks can be on either side, so pick one.
IsLeftMark = (sFirst = LeftDQ Or sFirst = "(" Or sFirst = "[")
' Like cannot match a right square bracket or paragraph mark
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr
If IsRightMark Then
' Reassign the working range to the previous word
Set r = r.Previous(Unit:=wdWord)
ElseIf IsLeftMark Then
' Optional reassignment to the next word
Set r = r.Next(Unit:=wdWord)
End If

' Assign the target range as the word before the working range
Set rTarget = r.Previous(Unit:=wdWord)
' Move the End of the target range backward over any punctuation
' marks, a paragraph mark, or whitespace
' Moving the End position works for the normal behavior while
' also accounting for several placement exceptions
sMarks = ",;:.?!—“”–()[] " + vbCr + vbTab
rTarget.MoveEndWhile Cset:=sMarks, Count:=wdBackward
' Position the empty target range before the previous word
rTarget.Collapse

' Copy the formatted text to the target location
rTarget.FormattedText = r.FormattedText

' Does the working range begin a regular sentence or a dialog
' sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)

' Delete the original word and maybe a trailing comma
' Include a comma if it follows the working range
sNext = r.Next.Text
If IsSentenceStart And sNext = "," Then r.MoveEnd Unit:=wdWord
r.Delete ' Delete the word
' Delete a trailing space if Word reinserted it
sFirst = r.Characters.First.Text
If sFirst = " " Then r.Delete

' Correct any spacing issues around the empty working range
' Prior delete steps preclude any trailing spaces
sPrevious = r.Previous.Text
sFirst = r.Characters.First.Text
' Does the empty working range precede some rightward or leftward
' punctuation characters?
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "[" Or _
sPrevious = vbCr
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr
' Insert a missing space after the working range if needed
If sPrevious = " " And IsRightMark Then
' Correct an extra space before the empty working range
r.Previous.Delete
ElseIf Not (sPrevious = " " Or IsLeftMark Or IsRightMark) Then
' Only thing left to check is a previous space
' Correct a missing space after the empty working range
r.InsertAfter Text:=" "
End If

' Correct any spacing issues before the target range
sPrevious = rTarget.Previous.Text
' Does the target range begin a paragraph or a text group?
' Left bracket character set match notation for Like is messy
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "[" Or _
sPrevious = vbCr
' Insert a missing prior space at the target range if needed
' No need to check an extra prior space based on moves above
If Not (sPrevious = " " Or IsLeftMark) Then
rTarget.InsertBefore Text:=" " ' Add the missing space
rTarget.MoveStart ' Trim the new space
End If

' Insert a missing space after the target range if needed
sLast = rTarget.Characters.Last.Text
sNext = rTarget.Next.Text
' Does the empty working range precede some rightward punctuation
' marks, a paragraph mark, or a space?
IsRightMark = sNext Like "[,;:.?!—”–)]" Or sNext = "]" Or _
sNext = vbCr
' Correct any spacing issues around the working range
If sLast = " " And (sNext = " " Or IsRightMark) Then
' Delete the unnecessary space at the end of the target range
rTarget.Characters.Last.Delete
ElseIf Not (sLast = " " Or sNext = " " Or IsRightMark) Then
' Insert a missing space after the target range
rTarget.InsertAfter Text:=" "
End If

' Correct any capitalization changes based on the working range
' beginning a sentence
' Does the new working range position begin a regular sentence or
' a dialog sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)
If IsSentenceStart Then
' Capitalize the new word at the original location
r.Case = wdTitleWord
' Capitalize the moved word
rTarget.Case = wdLowerCase
End If

' Correct any capitalization changes based on the target range
' beginning a sentence
' Does the target range position begin a regular sentence or
' a dialog sentence at a left double quote?
sPrevious = rTarget.Previous.Text
IsSentenceStart = (rTarget.Start = rTarget.Sentences.First.Start) Or _
(sPrevious = LeftDQ)
If IsSentenceStart Then
' Correct the target word capitalization
rTarget.Case = wdTitleWord
' Correct the capitalization of the word after the target range
rTarget.Next(Unit:=wdWord).Case = wdLowerCase
End If

' Finish with the moved word selected
rTarget.Select
End Sub

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.

Sub MoveWordRight()
' Move the current word forward one in the document while
' allowing grouping symbols and dialog
' Dialog is treated as a sentence start
' No capitalization checks are performed

' Declare several string variables (optional)
' Each variable will store a single character for comparison
Dim sFirst As String, sLast As String
Dim sPrevious As String, sNext As String
' Declare a string variable to store all relevant special characters
' or punctuation marks (optional)
Dim sMarks As String
' Declare some conditional variables (optional)
Dim IsSentenceStart As Boolean
Dim IsLeftMark As Boolean, IsRightMark As Boolean
' Declare the working and target ranges (optional)
Dim r As Range, rTarget As Range

' Set the working range to the current document selection
Set r = Selection.Range

' Expand the working range over the current word
r.Collapse ' Avoid any selection issues (optional)
r.Expand
' Adjust the working range to a more intuitive word range
sFirst = r.Characters.First.Text
' Does the empty working range precede some rightward or leftward
' punctuation characters?
' Some marks can be on either side, so pick one.
IsLeftMark = (sFirst = LeftDQ Or sFirst = "(" Or sFirst = "[")
' Like cannot match a right square bracket or paragraph mark
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr
If IsRightMark Then
' Reassign the working range to the previous word
Set r = r.Previous(Unit:=wdWord)
ElseIf IsLeftMark Then
' Optional reassignment to the next word
Set r = r.Next(Unit:=wdWord)
End If

' Assign the target range as the word before the working range
Set rTarget = r.Next(Unit:=wdWord)
' Move the End of the target range backward over any punctuation
' marks, a paragraph mark, or whitespace
' Moving the End position works for the normal behavior while
' also accounting for several placement exceptions
sMarks = ",;:.?!—“”–()[] " + vbCr + vbTab
rTarget.MoveStartWhile Cset:=sMarks
' Position the empty target range before the previous word
rTarget.Collapse Direction:=wdCollapseEnd

' Copy the formatted text to the target location
rTarget.FormattedText = r.FormattedText

' Does the working range begin a regular sentence or a dialog
' sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)

' Delete the original word and maybe a trailing comma
' Include a comma if it follows the working range
sNext = r.Next.Text
If IsSentenceStart And sNext = "," Then r.MoveEnd Unit:=wdWord
r.Delete ' Delete the word
' Delete a trailing space if Word reinserted it
sFirst = r.Characters.First.Text
If sFirst = " " Then r.Delete

' Correct any spacing issues around the empty working range
' Prior delete steps preclude any trailing spaces
sPrevious = r.Previous.Text
sFirst = r.Characters.First.Text
' Does the empty working range precede some rightward or leftward
' punctuation characters?
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "[" Or _
sPrevious = vbCr
IsRightMark = sFirst Like "[,;:.?!—”–)]" Or sFirst = "]" Or _
sFirst = vbCr
If sPrevious = " " And IsRightMark Then
' Correct an extra space before the empty working range
r.Previous.Delete
ElseIf Not (sPrevious = " " Or IsLeftMark Or IsRightMark) Then
' Only thing left to check is a previous space
' Correct a missing space after the empty working range
r.InsertAfter Text:=" "
End If

' Correct any spacing issues before the target range
sPrevious = rTarget.Previous.Text
' Does the target range begin a paragraph or a text group?
' Left bracket character set match notation for Like is messy
IsLeftMark = sPrevious Like "[(“—]" Or sPrevious = "[" Or _
sPrevious = vbCr
' Insert a missing prior space at the target range if needed
' No need to check an extra prior space based on moves above
If Not (sPrevious = " " Or IsLeftMark) Then
rTarget.InsertBefore Text:=" " ' Add the missing space
rTarget.MoveStart ' Trim the new space
End If

' Insert a missing space after the target range if needed
sLast = rTarget.Characters.Last.Text
sNext = rTarget.Next.Text
' Does the empty working range precede some rightward punctuation
' marks, a paragraph mark, or a space?
IsRightMark = sNext Like "[,;:.?!—”–)]" Or sNext = "]" Or _
sNext = vbCr
' Correct any spacing issues around the working range
If sLast = " " And (sNext = " " Or IsRightMark) Then
' Delete the unnecessary space at the end of the target range
rTarget.Characters.Last.Delete
ElseIf Not (sLast = " " Or sNext = " " Or IsRightMark) Then
' Insert a missing space after the target range
rTarget.InsertAfter Text:=" "
End If

' Correct any capitalization changes based on the working range
' beginning a sentence
' Does the new working range position begin a regular sentence or
' a dialog sentence at a left double quote?
sPrevious = r.Previous.Text
IsSentenceStart = (r.Start = r.Sentences.First.Start) Or _
(sPrevious = LeftDQ)
If IsSentenceStart Then
' Capitalize the new word at the original location
r.Case = wdTitleWord
' Capitalize the moved word
rTarget.Case = wdLowerCase
End If

' Correct any capitalization changes based on the target range
' beginning a sentence
' Does the target range position begin a regular sentence or
' a dialog sentence at a left double quote?
sPrevious = rTarget.Previous.Text
IsSentenceStart = (rTarget.Start = rTarget.Sentences.First.Start) Or _
(sPrevious = LeftDQ)
If IsSentenceStart Then
' Correct the target word capitalization
rTarget.Case = wdTitleWord
' Correct the capitalization of the word after the target range
rTarget.Next(Unit:=wdWord).Case = wdLowerCase
End If

' Finish with the moved word selected
rTarget.Select
End Sub

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.

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.