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 multiple paragraphs

Word • Macros • Functions
Peter Ronhovde
28
min read

When connecting a Dragon Professional script to Word, the easiest and most robust approach is to call a Word VBA macro to accomplish the task within the application. We rely on the interconnection between the applications mostly to invoke the desired macro. The Dragon Professional script is the manager for the task, and Word is the worker.

So we wish to create a Word function that moves one or more paragraphs up or down in the document by a specified number of paragraphs.

Thanks for your interest

This content is part of a paid plan.

Move Multiple Paragraphs

Microsoft Word comes with a stock command and associated keyboard shortcut (Alt+Shift+Up or Down arrows in Word for Windows and Control+Shift+Up or Down on a Mac), to move one or more paragraphs up or down in a document. I use them every day. The shortcuts are technically for outlines, but they work just fine for any text despite in the category in the keyboard shortcuts documentation page.

What’s different today is we need to implement our own variation to automatically select and move any number of paragraphs, so we can connect it to a Dragon script using a voice command. The emphasis in this article is on the Word VBA macro but see a previous article for a Dragon Professional script than can run it via voice.

We’ll further allow sloppy paragraph selections. This extra touch is convenient for some macros that have a clearly defined effect. The standard command mentioned above also works like this, so we might as well adopt the same behavior.

Create function skeleton

A previous blog post covers the steps to create an empty function. When you’re done, the skeleton for this macro will be something like:

Function MoveParagraphs(NParagraphs As Long, _
Optional BSelected As Boolean = True) As Range
' Move current paragraph(s) up or down in document by NParagraphs
' BSelected indicates whether moved text is selected when done

Set MoveParagraphs = Nothing ' Temporary value
End Function

As a reminder, the single quoted lines tell VBA the rest of the text on that line is a comment meant for human readers. Also, an underscore _ character tells VBA we're continuing the VBA step/command on the next editor line to improve readability for any humans. The editor does not care as long as we do not exceed 24 line continuations for a single command.

No keyboard shortcut here

Since this subroutine has parameters inside the parentheses, we won’t be able to assign it directly to a keyboard shortcut or a custom ribbon or quick launch bar button in Microsoft Word. However, we can still use the macro directly from other macros or call it using a Dragon Professional voice command script.

What return type?

Given it’s main purpose, this function doesn’t require a return type, but it is convenient to return the range of the moved paragraphs. Toward this end, we added “As Range” after the parameter parentheses to tell VBA the returned value will be a document range.

Function MoveParagraphs(...) As Range

At the end of the function, we temporarily set the return value to Nothing since we haven't established the returned result.

Set MoveParagraphs = Nothing ' Temporary value

Remember in VBA functions, we assign the returned value using the function name. We need to use Set because a range is an object. We can actually set the returned value anywhere within the function, but often this ends up being at the end because we are done with all changes at that point.

What parameters?

We have two parameters, but we have a few extra details to unpack with the second parameter.

How many paragraphs to move?

The first parameter is obviously the number of paragraphs to move which is named appropriately:

Dim NParagraphs As Long

Recall VBA uses Dim to declare a variable with a type. It is required if Option Explicit is set for the VBA module. Otherwise, VBA will infer the type based on what data is assigned to it.

Not an Integer …

I used to use Integer numbers in similar macros because it makes more sense in terms of the possible number of paragraphs in a document. Integers in VBA have a maximum value of about 32,000 which probably includes most cases for a writer when using this function. We would probably never want to give the command: “Move that up 36513 paragraphs.” We’d find a different solution.

Use Long numbers (most of the time)

I started using a Long number type in most macro number parameters to avoid issues with parameter types clashing. Parameter types must match exactly when calling them with the Run method of the Application object (like we do from a Dragon Professional script). Such errors are annoying, and the tiny bit of extra storage required to use a Long value will never be noticed for a VBA macro.

I like to precede number variables with an “n” to remind myself what category of variable they are. This is just a personal preference and is not required. Further, a capital first letter “N” often informally indicates (to me) a constant value that will not change inside the subroutine, but I often violate the second preference for articles to make the variable names prettier.

End macro with selected paragraphs?

When working on a document, I prefer to finish the macro by selecting any moved text as a visual indicator of the change. However, we may not want this effect if we’re calling it from a voice command script.

What do we do?

We could just require a second parameter.

BSelected As Boolean ' probably annoying ...

We need the parameter for a complete macro, but it’s a tad annoying to have to always specify our choice when we’ll use True most of the time.

I preceded the variable name with a “B” as a reminder that it is a Boolean type. As mentioned above, this is just a personal preference.

Optional parameters

We finally have a reason to talk about optional parameters.

Since both options are necessary but we will usually select the final moved text, we can simply include it as an optional parameter. We literally precede the parameter name with the keyword Optional.

Optional BSelected As Boolean ' not quite done ...

Since it’s either a yes or no (True or False) value we use a Boolean data type.

Finally, if it’s optional, we need to specify a default value in case (probably most of the time) the user does not give one. We add an “= SomeDefaultValue” after the data type but still inside the parentheses. Now the second parameter reads as follows:

Optional BSelected As Boolean = True

The default value must a fixed value like a number or plain text. Some constant object values are okay like Nothing, but it cannot require a function call (with rare exceptions) or any other calculation when the function is used (called a "runtime evaluation").

Calling the macro?

As advertised, the optional parameter allows us to call the function but not give that parameter.

Call MoveParagraphs(-3)

The above command will move the current paragraph up three in the document. Since the optional argument is omitted, the macro will assume a True value for the variable inside the macro and end with the moved text selected. However, a user could instead override the default value and call the subroutine using:

Call MoveParagraphs(2, False)

This will move the current paragraph down two in the document (positive 2 means move the current paragraph forward by two paragraphs in the document). False as the second parameter indicates the macro should not select the moved text before it finishes.

Get current paragraph Range

We’ll need two Ranges variables for our task: one to identify the initial document paragraph(s) where the user is working and a second to mark the target moved-text position.

Dim rParagraphs As Range, rTarget As Range

Remember, if we want to declare two variables on the same line, we simply separate them by commas. Both variables need their own type given even if they are the same type. Otherwise, a variable with a missing type will be assigned a generic Variant type.

The rParagraphs range will be set to span the initial partially selected paragraphs, but we’ll move the target range to the intended position.

Define paragraph(s) range

Define the paragraph range as the initial Selection range.

Set rParagraphs = Selection.Range

This also works if there is just an insertion point (the blinking I-bar) since that is basically a Selection that spans no document text (kind of, but the details do not matter here).

Recall we must use Set because a range is not just a plain value like a number or some plain text. As mentioned previously, I like to precede my range variables with an “r” as a reminder of what type of data the variable stores.

Ranges, like other objects in VBA, have various properties as well as methods or actions they can carry out on their content. We needed to access the Range property of the Selection because the Selection includes more information than just a Range of spanned document content.

Technically, we could let the Selection act like one of the ranges since it is almost the same thing, but using two ranges makes this function cleaner to implement. We also don’t modify the visible Selection on the screen until we’re ready to do so.

Allow a sloppy selection

The easiest method to capture the range of the paragraph (s) around the initial selection or insertion point is to use the range’s Expand method.

rParagraphs.Expand Unit:=wdParagraph

We specify a Unit using the standard paragraph constant wdParagraph. While it is not documented explicitly, Expand will extend the range both directions even if the initially selected text spans more than one paragraph.

Alternative methods

I was a little lazy above, so let’s expound on it just a little bit.

I am usually a apprehensive when utilizing a possible side-effect (selecting multiple paragraphs using Expand) of a method. The Microsoft documentation is not explicit (terse is an understatement for some Microsoft VBA documentation pages) about how the expansion is carried out, so we’re kind of guessing. If you prefer to be clear and explicit, you could instead use:

' Alternative methods to expand paragraph(s) range
rParagraphs.StartOf Unit:=wdParagraph, Extend:=wdExtend
rParagraphs.EndOf Unit:=wdParagraph, Extend:=wdExtend

When using StartOf and EndOf, the Start or End positions of the range will not move if already at the respective location. In practice, the lazy version above seems to do the same thing, but I cannot guarantee it since the documentation is terse for the command.

No Selection change yet

We haven’t yet changed the Selection or insertion point, but with either expansion approach, the rParagraphs range now invisibly spans the entirety of any paragraphs connected to the initial Selection.

Move the target range

Now we want to position the target range at the intended location. It’s convenient to duplicate the paragraphs range now.

Set rTarget = rParagraphs.Duplicate

Unfortunately, there is a slight asymmetry in the logic depending on whether we’re moving up or down, so duplicating the paragraphs range at this moment sidesteps a subtle movement bug (computer speak for a logical error) by taking advantage of the fact rParagraphs spans one or more full paragraphs.

The Duplicate method ensures we have a new independent range. Otherwise, the two range variables would literally refer to the same range in the document. Meaning, as we move rTarget to the target location below, the rParagraphs range would also change. We don’t want that.

Move to the target paragraph

Now we want to move the target range to the intended copy location. The obvious method is the Move command.

rTarget.Move Unit:=wdParagraph, Count:=NParagraphs

We’re moving by paragraphs, so we needed to specify a Unit as wdParagraph. The Count is given as a macro parameter above as NParagraphs, so we include that value here.

Moves are initiated from the beginning of the range, so we don’t have to worry about tiny details (the Selection has an “active” side).

Up or down?

Negative values indicate moves backward, and positive values correspond to forward movement in the document. This may seem a little counter-intuitive at first, but it makes more sense when you think about how the document grows downward as you add content.

Moves collapse the range

After the move, the range collapses. That is, it spans no text (but there is a subtlety we’ll ignore for now), so its Start and End positions are the same in the document.

Subtle gotchas

This is the move that would result in a small bug if we were not careful. The target position movement works both directions with exactly the same command only because the range spans the entirety of the paragraph(s). When starting from the beginning of a paragraph, a paragraph Unit step moves up or down by exactly one paragraph. If we did not start at the beginning of the paragraph, the first up step would move to the beginning of the paragraph not to the previous paragraph.

See the Gotchas segment below if you’re also thinking ahead about possible problems like what happens if NParagraphs is zero.

Copy formatted text

We need to copy the original paragraph(s) text to the target location. We accomplish this using the FormattedText property.

rTarget.FormattedText = rParagraphs.FormattedText

We literally just set the two properties for the ranges equal.

What about Text?

There is also a Text property, but that would copy the text without any formatting.

rTarget.Text = rParagraphs.Text ' Loses at least character formatting ...

Delete original text

Now we want to delete the original paragraph(s) text since it was “moved” to the new location. Just use the Delete method of the initial paragraphs range.

rParagraphs.Delete

End with moved text selected

As previously advertised, we want to end the macro with the moved target text selected as a visual indicator of the change. Literally just use a Select method of the modified target range.

rTarget.Select

But only if user wants it

However, we only want to select the text if the user specified it. This is a user-interface decision and is not required, but you’ll probably regret not using sometime when you’re squinting at the screen, trying to figure out where the text went.

How?

We check the BSelected argument value the user provided when he or she called the macro (see above). We need a conditional statement to check the value of BSelected and only select the text if it is True.

If BSelected Then rTarget.Select

We don’t need to check using BSelected = True.

If BSelected = True Then rTarget.Select ' not necessary

Although it is not technically incorrect, BSelected already stores a Boolean (True-False) value, so we can just refer to it directly.

Benefits of ranges

We use two independent ranges, rParagraphs and rTarget, effectively as invisible selections in the document. Each range served a different purpose during the macro, but both variables evaporate into nothingness when the macro finishes, leaving us with only the document changes.

Gotchas?

The old “what could go wrong” segment …

No paragraphs?

What if someone tells the macro to move by zero paragraphs?

In this macro, we’re probably fine, but we should still think about things like this. There is no reason to continue through all the steps and copy formatted text to the same location in the document. If we’re given zero paragraphs for the move, nothing should happen; and we should just exit the macro.

Use a conditional statement to check the number of paragraphs at the beginning of the function. The condition is:

NParagraphs = 0

Exit the function

If the given number of paragraphs is zero, just exit the subroutine.

If NParagraphs = 0 Then
Exit Sub
End If

Set a valid returned range

However, we also need to set a returned value since it is a function. Moving a paragraph by zero paragraphs isn’t an invalid action. It’s just a logical issue. Setting the returned result to Nothing would be okay, indicating the user made a mistake, but it is probably excessive since Nothing generally means an invalid or undefined object (a range in our case).

With that in mind, returning the starting paragraph range makes sense and would be consistent with a “zero paragraph move.” We called this range rParagraphs earlier.

' Zero paragraph move is the original paragraph range
Set MoveParagraphs = rParagraphs

This means we need to define the rParagraphs range first (done below) before we check whether the user gave us a zero-paragraph move. The combined fool-proof check is now:

If NParagraphs = 0 Then
' Zero paragraph move is original paragraph range
Set MoveParagraphs = rParagraphs
Exit Sub
End If

From there, we are confident we have a valid paragraph move number.

Finish the macro

Now we just put the commands together.

Function MoveParagraphs(NParagraphs As Long, _
Optional BSelected As Boolean = True) As Range
' Move current paragraph(s) up or down in document by NParagraphs
' Allows sloppy paragraph selections
' BSelected indicates whether the moved text is selected when done
' Returns the moved paragraph range

' Define initial paragraph range based on Selection or insertion point
Dim rParagraphs As Range, rTarget As Range
Set rParagraphs = Selection.Range
' Expand range to cover entirety of the starting paragraph(s)
rParagraphs.Expand Unit:=wdParagraph

' Just exit if given zero paragraphs to move
If NParagraphs = 0 Then
' Zero paragraph move is original paragraph range
Set MoveParagraphs = rParagraphs
Exit Function
End If


' Define target position relative to initial paragraph range
Set rTarget = rParagraphs.Duplicate
' Move target range to the intended paragraph
rTarget.Move Unit:=wdParagraph, Count:=NParagraphs

' Move formatted paragraph text to the target location
rTarget.FormattedText = rParagraphs.FormattedText
rParagraphs.Delete ' Delete original text

' End with moved text selected if desired
If BSelected Then rTarget.Select

' Set returned value to moved text range
Set MoveParagraphs = rTarget
End Function

Since this macro has parameters, we cannot assign it directly to a keyboard shortcut or to a custom ribbon or quick launch bar button in Word, but we can conveniently run it from another Word VBA macro or when using a Dragon Professional voice command script (see related article).

The returned value probably won’t be used much in other macros or scripts, but it is convenient if needed. It also allows us to chain commands on a single line if desired.

Undo process …

Undoing this macro would occur in two steps: undo the delete action and then the copy step.

This isn’t too bad, but the improved version in the extended content below packages these into a single undo step. This behavior feels more intuitive, but it does come with extra baggage in order on the make sure it’s done correctly.

What else could we do?

Personally, I have other variations that moves the paragraphs to the beginning or end of a nearby heading or scene, but that is a lesson for another day. I also have a variation that moves the paragraphs to an “extras” section at the end of my novel or notes document. It provides a quick way to remove possibly extraneous content without actually losing it.

Improvements

This macro is nice as is, but I think it would benefit from a custom undo record (see our summary article for more details), so read on if you’d like it to behave as cleanly and intuitively as possible.

Add an undo record

Many logical steps are performed practically with smaller individual steps—one brick at a time. The Undo feature in Word tracks them all individually unless told otherwise.

The above MoveParagraphs function uses two main actions to accomplish its overall task: copy some text to another location in the document and then delete it from the original position. Using an undo record combines these into a single, intuitive step. That way the user can reverse both as one much like many other commands in Word.

Of course, there are other steps in between where we manipulate the respective range variables to properly carry out the actions, but these don’t result in any document changes, so Word doesn’t track them in its list of undo-able actions.

Warning: Rocking chairs ahead … and you’re the cat

Undo records are nice and make our macros feel more like native Word commands, but they need to be handled with care. If an undo record isn’t ended properly, we can lose actual novel text.

Yep, you heard me.

This isn’t supposed to happen since the records are supposed to end automatically even if something goes wrong, but they don’t always. It also doesn’t sound like it should be a problem in the days of cloud computing since the cloud-saved document should have a version record, right?

Well … not always, and it’s happened to me several times.

I definitely use undo records but be deliberate when implementing them. Just pretend you’re a cat in a roomful of rocking chairs.

Create the undo record

An UndoRecord is a special data type that stores internal information to allow Microsoft Word to reverse more than one document change at a time. That is, when you press Control+Z (or Command+Z on a Mac), all the changes for that record are undone at the same time.

We need to declare and Set an UndoRecord variable to create a valid undo record.

Dim MyUndo As UndoRecord

The variable now exists, but it literally refers to Nothing. Let’s connect it to Word’s Undo “stack.”

Set MyUndo = Application.UndoRecord

At this point, MyUndo is just some data stored in memory but not associated with any user activity. We now tell word to begin recording actions to undo using the method StartCustomRecord.

MyUndo.StartCustomRecord("VBA Move paragraph(s)")

We give it a plain text string as the undo record a name. This name will appear in the list undo items which is nice. The record name is optional, but there is no upside to leaving it out other than saving a few words of typing now.

Better record name for clarity

The above record name is now waiting for undo-able actions to record, but I like to take it a tiny step forward to be more specific. I want to include how many paragraphs up or down were moved for clarity.Why?

Why?

If the user runs the macro several times on different paragraph groups, the undo record name would carry a little more information about what happened. It's a nominal effect, I admit, but I like the detail. LIke an unnecessary swoosh of a pen on a signature. Just looks good.

The UndoRecord name must be in plain text, so we need to convert the Long data type of the NParagraphs number to a plain text string. The CStr() function converts a number value into plain text. We then store this text number in a String variable sNumber.

Dim sNumber As String
sNumber = CStr(NParagraphs)

Remember, after using CStr(), the number will be plain text. We then add (called concatenate with strings) the previous generic record name to our plain number text and store the result in another plain text variable sRecordName just to keep things a little clearer.

Dim sRecordName As String
sRecordName = "VBA Move paragraph(s) " + sNumber
MyUndo.StartCustomRecord(sRecordName)

This custom name is not required when creating undo records, but it is clearer if the user changes his or her mind, particularly after having given several other commands.

Messier version

I created a second plain text string sRecordName above to store the record name, but neither name is required. Would could just chain everything together in one statement.

' Messier version that omits the string variables
MyUndo.StartCustomRecord("VBA Move paragraph(s) " + CStr(sNumber))

It’s a matter of preference. Do you want the cleaner, multi-line version or the concise, but messier, single-line version? I tend to err on the side of the latter for my own macros, but I lean toward the former for articles.

End the undo record

At the end of the macro, we need to tell VBA we’ve finished the undo record. This is important.

MyUndo.EndCustomRecord ' End the custom undo record

Disable screen updates

This is an extra little tweak, but it does avoid the possibility of some choppy visual behavior if your computer is running slowly. The two main settings I like to toggle for a macro with larger or many text changes are ScreenUpdating and Pagination.

' Toggle screen updates off
Application.ScreenUpdating = False
Options.Pagination = False

Then at the end of the macro, just toggle them back on.

' Toggle screen updates off
Application.ScreenUpdating = True
Options.Pagination = True

Since this happens a lot across various macros, I like to create two little functions. The result below encapsulates them into two kitten subroutines.

If you want a few more details, a more general version further toggles spelling and grammar checking (which is probably overkill, but don’t tell anyone else). I also created a more comprehensive approach if you’re interested but only if you're really into complex workflows.

Handle any errors

The chances of something going wrong are probably low in this macro, but if you’re being careful when using undo records, you should add an “error handler.”

Add an error handler

Tell VBA you want to watch for any error when the macro runs.

On Error GoTo MoveError

On Error tells VBA we want to watch for errors. GoTo instructs VBA to move to a specific line named MoveError (my name; choose what you wish) if an error occurs.

Exit the function before the error steps

Since we’re creating some extra steps to run specifically for any possible error, you should probably exit the macro before the error steps.

Exit Sub ' Do not run any following error steps

This is not strictly necessary, since we could just let the macro run all the way through the error steps every time, but I generally avoid that behavior.

Often there is significant overlap between any “finish the macro steps” and “an error occurred” steps, but usually at least one command is different, so I end up repeating the finishing steps. I don’t like the repetition, but it’s clearer to separate any error-related steps from the main macro.

Start the error steps

Then include the error steps usually at the end of the macro.

MoveError:

This line is just a label. It doesn’t actually do anything by itself, and VBA just ignores such lines if it ever encounters them during normal running.

Again, the following often repeats some closing steps from the main part of the macro, but the separation of error-specific steps is clearer this way.

End custom undo record

Now, end the undo record.

MyUndo.EndCustomRecord ' End the custom undo record

This is the big one.

Re-enable screen updates

If you toggled screen updates off, you probably want to toggle then back on here also.

' Toggle screen updates off
Application.ScreenUpdating = True
Options.Pagination = True

Word would probably do this automatically, and I’ve only encountered an issue a few times over years of working with macros, but it’s nicer to be clear about it rather than just hope Word catches any issues for you.

Set invalid returned value

Since an error occurred, a valid returned range does not exist since no text was reliably identified nor moved, so set the function name to Nothing to indicate this result.

Set MoveParagraphs = Nothing ' Set invalid returned value

With this result, a user could check whether the returned range is Nothing before using the result.

Clear the error

Tell VBA we’ve handled the error by clearing it. The standard VBA error object is Err, and we refer to the Clear method.

Err.Clear

If this isn't done, then the calling macro would crash if it does not handle the error.

Finish the improved function

This version looks more programmy than the earlier one, but it does behave a little better.

Function MoveParagraphs(NParagraphs As Long, _
Optional BSelected As Boolean = True) As Range
' Move the current paragraph(s) up or down in the document by NParagraphs
' Allows sloppy paragraph selections
' BSelected indicates whether the moved text is selected when done
' Returns the moved paragraph range

' Set up an error handler in case we have any problems
On Error GoTo MoveError

' Define initial paragraph range based on Selection or insertion point
Dim rParagraphs As Range, rTarget As Range
Set rParagraphs = Selection.Range
' Expand range to cover entirety of the starting paragraph(s)
rParagraphs.Expand Unit:=wdParagraph

' Just exit if given zero paragraphs to move
If NParagraphs = 0 Then
' Zero paragraph move is original paragraph range
Set MoveParagraphs = rParagraphs
Exit Function
End If


' Define target position based on paragraphs range
Set rTarget = rParagraphs.Duplicate
' Move target range to the intended paragraph
rTarget.Move Unit:=wdParagraph, Count:=NParagraphs


' Toggle screen updates off
DisableScreenUpdates ' Call existing function

' Define and create an undo record
Dim sNumber As String, sRecordName As String
sNumber = CStr(NParagraphs)
sRecordName = "VBA Move paragraph(s) " + sNumber
MyUndo.StartCustomRecord(sRecordName)

' Move formatted paragraph text to the target location
rTarget.FormattedText = rParagraphs.FormattedText
rParagraphs.Delete ' Delete original text

' End with moved text selected if desired
If BSelected Then rTarget.Select


MyUndo.EndCustomRecord ' End custom undo record
EnableScreenUpdates ' Call existing function

' Set returned value to moved text range
Set MoveParagraphs = rTarget

Exit Sub ' Do not run any error steps below


MoveError:
' Do these steps if an error occurs
MyUndo.EndCustomRecord ' End custom undo record
EnableScreenUpdates ' Call existing function

Set MoveParagraphs = Nothing ' Set invalid result

Err.Clear
End Function

Screen updates

If you don’t like the functions or don’t want to toggle the screen updates, just leave those steps out. Quick and dirty versions of the functions are given below but see the other article if you want more details.

Sub DisableScreenUpdates()
' Toggle screen updates off
Application.ScreenUpdating = False
Options.Pagination = False
End Sub
Sub EnableScreenUpdates()
' Toggle screen updates on
Application.ScreenUpdating = True
Options.Pagination = True
End Sub

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.