Making selections is a fundamental aspect of editing in a word processor, so anything that can expedite the process can help us edit faster. Tools to manipulate headings are limited in VBA compared to those available for paragraphs or words. Toward this end, we’ll create another tool for our toolbox in the form of a macro that quickly extends a heading selection up or down in the document over successive headings.
Thanks for your interest
This content is part of a paid plan.
Extend Heading Selection
In Microsoft Word, headings are basically a document range more like sentences or words rather than more fundamental document elements such as paragraphs or sections. We have some out-of-the-box tools to manipulate headings in the application and with the corresponding tools in Visual Basic for Applications (VBA). Both are rather limited compared to what we can do with other document elements like paragraphs or words (see previous article for more explanation). Mostly, we can only control heading paragraph styles and navigate to standard headings using the GoTo tab in the Advanced Find dialog.
Thus, we need to machine our own headings tools to facilitate our writing and editing processes. In this article, we focus on extending a heading selection to quickly select a range of chapters or scenes in a document which we can then move or delete as needed.
This macro leans toward my personal editing method where I outline a novel by chapter and scene as it grows to better track the story progression. If that’s not your style (no pun intended), it’s also useful in my novel and blog notes documents, so it should be general enough for anyone to benefit from it.
If you prefer to shortcut around the commentary, skip ahead to the macro explanation … otherwise, allow me to briefly sketch out my process along with some additional motivation for this macro.
My evolving novel outline
I usually maintain an overview of the evolving story in my head, but sometimes the details escape me. As a result, I began inserting chapter and scene headings as I write. I use the obvious story divisions including Acts, Chapters, and Scenes; but I eventually added Subscenes also since I discovered I wanted a more detailed view of how the scenes developed.
These styles are derived from the standard Word styles Heading 1 through 4, respectively, with unique paragraph styles to make them obvious at a glance. The fact they are “derived” heading styles makes creating macros a little more trouble than it might appear at first but see this previous article for more explanation.
Not a “plotter” outline
While I desperately want to plot my novels in detail, it just doesn’t work for me. My novel outline isn’t about nurturing or enforcing any specific plot or story structure. It’s much more about tracking the novel’s development as it grows and evolves, so I don’t get lost in my own story.
Extracting scenes is easier
Word’s expanding and collapsing heading levels are tedious to use for a novel in my opinion. When the details or even just the overall flow of the story become muddled in my mind, I use another macro to extract the outline and any relevant notes (see separate article on my novel notes macros) into a separate document. I use it as an overview to refresh the story progression in my mind. We’ll work through that extraction macro another day.
Reordering and reorganizing text
Incorporating a novel outline using headings also makes reordering content easier as the writing and the editing progresses. I prefer to keep the whole novel in a single document, at least until it becomes overwhelming, which allows me to move text between and adjust chapter and scene boundaries more easily. In Word for Windows, we can also drag and drop headings in the Navigation Pane although occasional formatting issues may occur.
Stripping novel notes and headings
If you’re concerned about all the extra fluff text “cluttering” your novel, another macro easily strips all headings and novel notes when I get ready to publish. I lose essentially nothing while maintaining an annotated and structured novel to guide me as we write and edit. We may cover this macro to strip headings and notes in a future article depending on the interest level.
Building “next” heading extension version from scratch
Except for an optional pair of functions to toggle screen updates, we will not rely on any previous functions for the essential features of today’s macros. We’ll instead work out all steps from scratch. Normally, this would be counterproductive since part of the point of creating functions is to save time and effort later, but we’ll explain more about why as we proceed.
Heading terminology
Let’s establish some common terminology. When I refer to a “heading paragraph”, I mean the first paragraph of a heading. The “heading content” is all the text including any subheadings below it. A “heading” is all the above.
"Derived" headings are headings that are based on one of the "standard" Word Heading 1 through 9 paragraph styles.
Macro skeleton
We have two macros today, one to extend the heading selection up and another for down. Our macro skeletons are:
The differences between the macros make combining into one messy (more like just jamming them together), so we’ll keep them separated. The internal steps of the two macros are similar, but the “previous” version requires extra steps to properly extend the selection backward in the document.
Macro naming “system”
The macro names may sound a little awkward, but it’s done with intention.
Naming pattern
I prefer a naming pattern including a general category usually consisting of an “action” and a “what”. The category is followed by any necessary details about the task. For example in these macros, the category action is “Extend”. The category what is “HeadingSelection” with two specific variations “Next” and “Previous”.
Capitalization
The word capitalization scheme is called “Pascal Case” (first letters of words are capitalized, and all words are jammed together). It just makes the name easier to read while including more details about what it does.
Do I always follow this naming pattern?
Well … my macro editing “library” has expanded over time while I learned more about VBA and my own editing process, so not really. All this is optional, but I try to create newer macros with better names that make them easier to find.
Don’t overthink it
The naming scheme is meant to help you organize your macros and better understand what they do at a glance. You’ll probably be surprised how fast your list of editing macros grows. Ignore all this if it’s confusing, but you’ll thank me if you use something like it earlier rather than later because sometimes I still need to scroll, scroll, scroll to find my own older macros.
Subroutines required for Word keyboard shortcuts
Since we plan to assign these macros to keyboard shortcuts in Microsoft Word, they must be subroutines, abbreviated “Sub” in VBA, without any parameters. If you happen to own Dragon Professional, custom voice command scripts that run Word macros aren’t restricted in the same way (see this previous article if you’re interested in learning more).
Set working ranges
We’ll need two range variable(s) to work with the document content.
- A range variable to track the current heading around the user’s starting position or selection. This content may or may not be selected when the macro starts. We’ll name variable this rCurrent.
- A range variable to locate and span the next heading range. We’re working forward in the document, so we’ll name this second range variable rNext. We’ll use rPrevious in the second macro.
I use rCurrent and rNext just to keep the names a little shorter than being even more descriptive like rCurrentHeading and rNextHeading since we already know we’re working with heading ranges. This is a balancing act of using names that mean something but not making them so long that they are tedious to use.
We can define both range variables on one line if we separate them by a comma.
I like preceding range variable names with an “r” to clearly indicate the type of variable within the macro, but this isn’t required.
Once each is properly identified, the intention is to merge the two ranges to complete the macro task.
What is a Range?
A Range variable is akin to an invisible Selection or insertion point (i.e., where the blinking I-bar is waiting for you to type text in the document). They share many features, such as being able to span text or move around the document, but range variables are not visible to the user unless document changes are made, and no information about them is saved when the macro ends.
Among other properties, Ranges have Start and End positions literally corresponding to character positions as counted from the beginning of the document. We can reference these positions as with any other property, and we’ll need them later.
What is the Selection then?
The Selection is the persistent object in VBA that tracks the user’s actual selection or position in the active document (the ActiveDocument is also a Word object in VBA). This applies whether the current document selection includes any spanned text or if it is just an empty insertion point. The Selection has additional properties (data) and methods (actions) associated with it allowing us to manipulate it in the document.
Any changes to the Selection are updated on the screen in real time while a macro runs. Rapid updates sometimes causes the screen to “flicker” which is why it’s usually better to avoid using the Selection for document manipulation in anything but the shortest macros.
If we can’t avoid using the Selection (as is the case for today’s macros), we can turn off screen updates (as well as document pagination) to hide any Selection changes while a macro runs (see previous article).
Sneaky Variant types
As an aside, both variable declarations above need to include the “As Range”, or the missing one will actually be a generic Variant data type. For example, if we left As Range off the first variable, it would look like:
On the other hand, a Variant variable can be anything, so this would still work in many macros as long as we’re not trying to pass the heading range to a macro or function requiring a range variable parameter (they are picky about specific types).
Get the current heading range
In our macro today, we need to get the heading range around the current user position in the document.
What is a bookmark (brief)?
At the application level, a bookmark is just a convenient way to mark and later jump to a specific position or content in a document. In VBA, a bookmark is like a range since it can span document content or mark a document position, but it contains more information defining what a bookmark is internally to Word.
Differences include Bookmarks can have names whereas ranges cannot other than what we name the variable. Bookmarks are also persistent in the document even after the macro finishes running. We've utilized this second feature in previous macros to quickly navigate a document.
Word also keeps an internal list of hidden, pre-defined bookmarks …
HeadingLevel bookmark
We access the current heading range using a pre-defined Word bookmark named \HeadingLevel (yes, the backslash \ is required at the beginning of the name). Since it’s a plain text name, we need to include it in double quotes when referencing it in the bookmarks collection.
This bookmark is invisible to the user in Word even in the GoTo tab of the Advanced Find dialog, but we can reference it in VBA using the Bookmarks collection of the Selection or the ActiveDocument. The (big) caveat is it only works for the heading range around the current Selection.
Required for derived heading ranges
This bookmark is the only reasonable way we can manipulate derived headings in Word VBA. Given its quirks, this is the main reason we’re using it in this macro since all my novel chapter and scene headings are derived from the standard Word headings 1 through 4.
Get the bookmark range
Bookmarks are like a range, but they’re not exactly a range. If we want to store the range corresponding to the \HeadingLevel bookmark, we need to reference its Range property.
Store the current heading range
We store the current heading range in the earlier range variable rCurrent.
Our range variable rCurrent is now assigned to the entire heading range around the initial position or selection in the document. Ranges (and other objects in VBA) require us to use Set because they contain more information than a typical value like a number.
Need the Start position
We technically only need the Start position of the current heading in this macro to properly extend the selected heading range later. Our available tools in VBA to navigate between derived heading styles are limited, so the easiest way to determine the Start position is to just span the entire heading range.
Current heading range choice
The correct choice for this range assignment is more subtle than it may seem at first glance. For instance, we could not assign Selection.Range to our rCurrent heading variable.
This range is correct if the user has already selected a heading range (such as occurs with repeated uses of this macro), but it may not span the whole initial heading in other circumstances. We would still need to access the range of the \HeadingLevel bookmark range to be sure rCurrent spanned the entire first heading under consideration which makes this assignment redundant.
An independent range
Our heading range rCurrent is independent of the Selection or even our other range variable unless specific changes are made to the document content inside the range. Using range variables in macros allows us to manipulate the content more easily.
Get the next heading range
We need to identify the tentative “next” heading range in preparation to merge it with the current range later. Next in our context implies the heading range following the last heading of the user’s initial selection. The idea behind this macro is to select successive headings, so in principle, the initial selection could span more than one heading.
Start with the current Selection
We initially set our next heading range rNext to the current user’s selection. We’ve done this before, so we need to reference the Range property of the Selection.
This is just a starting point.
Initial next heading range choice
The correct range assignment here is more subtle than it may seem at first. Using the initial selection as a starting point for rNext is essential to making the extension work correctly if it is to naturally extend over successive heading ranges.
Using initial heading only works for first heading extension
For example, we could not assign rCurrent as the initial rNext range.
This seems like an obvious choice, but it doesn’t work as a starting point for rNext when the user’s initial selection spans more than one heading range. rCurrent only spans the current (first) heading range, so the steps below would only identify the heading immediately after rCurrent as the “next” one. We want our extension to successively include more headings if we run the macro multiple times.
Initial partial heading selections
Another advantage of using Selection.Range as the starting point for rNext is it also works well when the initial selection spans only part of one or more headings. In either case, the following steps would just naturally extend our selection over the entirety of all of the headings.
Move to the next heading using Collapse
We need to move the rNext range from spanning the current user-selected text to the next heading range. Unfortunately, the regular Move methods of range variables do not have Unit constants referring to headings (see previous article for more explanation), but we can still effectively make the move by collapsing the selection toward the End position.
We needed to specify the Direction option as wdCollapseEnd because the default is to collapse toward the Start of the range.
If you’re not used to VBA in Word, you might squint at the screen a little at this command, but using the Collapse method for range movement is not uncommon in VBA.
What if rNext ends at another heading?
An important use case is extending the user selection over successive headings. In these cases, rNext would end at a new heading, so the collapse de facto moves the range to the next heading since the last heading selected logically ends at the beginning of the next one.
What if rNext ends inside a heading?
If rNext doesn’t span up to the end of a heading, then after the collapse, we’ll be positioned somewhere inside the last heading range. This is still mostly intuitive for this macro because the later extension will just expand to include the entire last heading.
No extra move necessary (for next version)
We do not need to include a follow-up move command to move “inside” the next heading.
The beginning of the heading is considered part of the heading, so we’re okay. However, we will need something like this move step when extending the heading selection backward in the document (see following macro below).
Selecting the next heading
Sit up straight because this gets a tad confusing to describe in text form (as if it hasn’t already).
The rNext range variable is now positioned at the beginning of the next heading, but we need to get the entirety of the range and reassign it to rNext.
Still need \HeadingLevel bookmark
Since we’re still using derived heading styles, we can only accomplish this using the pre-defined \HeadingLevel bookmark. Unfortunately as mentioned earlier, this bookmark only works for the heading range around the current Selection (we have no control over this requirement), but the Selection still resides inside the initial heading above … not with rNext.
Ughhh.
The Selection is not in the same heading as rNext, so if we use this mysterious Word bookmark, we’ll get the same range as earlier.
No help (so far).
Force Selection move with range Select
The rNext range is now positioned at the beginning of the next heading range, so we need to select it to force the Selection to move into the same heading.
Now Word automatically updates the \HeadingLevel bookmark in the background, and we can use the bookmark to span the next heading range.
This step acts as if the user intentionally clicked at the same location as rNext in the document. It just happens at computer speed in VBA. As a result, this step may cause the display to flicker since Word will update this on screen immediately, but a later step again changes the Selection in rapid succession.
I cringe when including this step, but we have no choice if we want to work with derived heading paragraph styles.
Store the next heading range
The Selection is now properly positioned at the same document position as our rNext range—the beginning of the next heading. We again access the \HeadingLevel bookmark in the Bookmarks collection of the Selection. Then using the bookmark’s Range property, we store (update) the next heading range in the rNext range variable.
This range is independent of rCurrent or the Selection.
Extend the range
We now have two range variables we want to merge. The first, rCurrent, stores the current (original) heading range; and rNext stores the full heading range after the initial selection. Our extended selection should start at the top of the initial heading and extend downward in the document to the end of the next heading.
How do we assign this in VBA?
Ranges have Start and End positions which we reference just like any other property:
More specifically, our extended selection begins at the Start position of the initial heading at rCurrent.Start. At the bottom, we use the End position of the next heading range corresponding to rNext.End.
Reassign the extended range
There are several ways to define the extended range, and all are equivalent. We’ll reference the SetRange method just to keep the command on one line. We’ll also reassign the extended range to the rCurrent range, so we don’t need to define a new range variable.
The Start and End options just correspond to the new Start and End positions of the referenced range rCurrent. Command options with assigned values require a colon-equals := symbol rather than just an equals = sign (see alternative assignments below). Multiple options must be separated by commas.
Alternative longer version
The SetRange method is just a shorthand command to set the Start and End positions of a range. It saves us from using two lines for the position assignments like:
Here, we use equals = signs because the Start and End range properties are document positions which are just regular numbers.
Select the extended range
Currently, this newly extended range variable is still invisible to the user, so we need to select it to make the changes visible on screen.
That’s it as far as a necessary macro logic.
Using the Selection is (usually) bad
In general, manipulating the Selection directly in a macro is not the best choice. It’s slower overall since all changes are updated on the screen in real time while the macro runs which is not ideal (but we can work around this aspect below).
Moreover, only one Selection exists in each document. Earlier, we defined two range variables to work within our active document which made the logic easier to work through. Some macros might not even work using only the Selection.
Why use the Selection in these macros then?
If you missed the comments above, we’re manipulating the Selection directly in this macro because we need to access Word’s predefined \HeadingLevel bookmark. This bookmark spans the heading range around the current user Selection in the document.
The \HeadingLevel bookmark range is updated automatically by Word in the background even while a macro is running. It is invisible to a user outside of VBA, but unfortunately, it is only available in VBA using either the Selection or the ActiveDocument.
Disable screen updates while running
A previous article explains how to disable screen (as well as document pagination) updates inside a VBA macro. Since we’re manipulating the Selection directly (see above comments), we can mitigate any screen flicker issues by disabling screen updates while the macro runs. The macro name is:
Then just before the macro finishes, we re-enable screen updates. The second function is:
We don’t need to use “Call” before either macro because they require no arguments.
Not a serious issue in this macro
I’ve used these heading extension macros below both with and without screen updates, and any screen flicker is minimal, so I would not consider this an essential addition. It is, however, a good practice just to be clean and clear.
Just be careful when debugging your macros. Although, even then, the chance of a problem is probably minimal since Word doesn’t seem to like leaving screen updates off after a macro runs, but it can happen.
Gotchas
What could go wrong in this macro? There are several things to consider in this pair of macros.
Range subtleties of this macro
This macro has at least three places where it’s easy to get off track or to make a subtle logical error that only works for certain use cases. It’s a good example of why you should playtest your macros (yes, sometimes creating macros is “playtime”; just admit it.) before assuming they’re done.
A related issue is when the initial Selection only partially spans two or more headings. Fortunately, the existing macro logic just selects both (or all) of the partially spanned headings, so no problem exists here.
What if the \HeadingLevel bookmark doesn’t exist?
Since \HeadingLevel is a bookmark, it might not exist … right? Not all documents have headings—
Well, it is a pre-defined Word bookmark.
In my testing, I’ve had no problems using the macros even in new documents with no headings and limited text, so Word seems to account for missing headings. In the interest of keeping the macro simpler, I omitted any range validation steps based on this pre-defined bookmark.
What if no “next” heading exists after the current one?
If no heading exists after the current one, the macro simply doesn’t move the next range variable rNext forward in the document, and the Selection doesn’t change. This is validated with testing, so no problem exists here.
What about higher priority heading levels?
Headings exist in levels 1 (highest priority) to 10 (body text). Since we’re working with headings, our macros will eventually encounter heading ranges with different levels. How do the macros handle them?
Next heading range version
Yeah, if the next heading range is a higher priority heading level, the macro will extend over the entire heading content including any subheadings.
This is recognized but accepted as a reasonable way for the macro to work—
But … shouldn’t we “fix” it?
Okay, since you brought it up, let’s think about it a little bit longer. A slew of questions comes to mind:
- What does the user want in each use case?
- Why “shouldn’t” it expand over the entire next higher priority heading?
- Is extending over the same heading level always correct?
- What makes the most sense?
- What is the “right” way for it to work?
We could try to somehow restrict the extension to the “top” of the higher priority heading content, but that doesn’t make sense to me. In my mind, no clear argument or need exists to restrict the extended range to just part of a higher priority heading it encounters.
The “right” solution isn’t obvious or even clear enough to me to spend the time and effort to “fix” it.
Sometimes just make a choice
Sometimes we just need to decide how the macro works. As long as that choice is reasonable and relatively intuitive, it’s probably okay.
Previous heading range extension version
If the backward (previous headings) version encounters a higher priority outline level, it will automatically expand over the entire higher level heading range. Similar to the above argument for the next heading version, this is reasonable behavior, so we won’t override it.
Final extend to next heading range macro
Putting the steps together, our heading range extension macro is:
Modifications for previous heading range version
When extending backward in the document instead, the bulk of the macro steps carry over with appropriate obvious modifications to account for the change in direction. We will only cover the differences to shorten the presentation (no, that's not sarcasm).
New previous range variable
A variable name change is trivial, but for completeness, we use rPrevious to represent the previous heading range over which we will extend the current heading selection.
Get previous heading range
The next step that changes is when we move to the previous heading.
Collapse toward Start position
We move the rPrevious range by collapsing it, but we need to move toward the beginning of the document. This means we need to collapse toward the Start position of the range.
Given the next step is a move, this step is actually redundant (the following move will initiate from the start of the range), but it is still clearer to include it.
Move into previous heading range
When we collapse toward the Start of the range, we’re still inside the current range, so we need to move our collapsed range into the previous heading content. It is sufficient to move backward by a single character, so we use the Move method.
As discussed in other articles, the Move method can take a Unit option which can be any value in this standard Units table. We choose a character step size with wdCharacter. We then specify the Count option to be negative 1 since the default is to move forward by one unit. A negative value moves backward in the document.
This move places the collapsed range just to the left of the last paragraph mark in the heading, so the \HeadingLevel bookmark used later will properly recognize the previous heading range.
Encountering a higher heading level
When we’re moving backward in the document, we can encounter headings with a higher priority level than the current heading level. How does our selection extension macro handle this? How should it work?
The previous heading range identification using the \HeadingLevel bookmark would naturally expand across the entire heading including any subheading content. In this macro, the expanded range would encompass our current rCurrent heading range.
This is acceptable behavior to me, so the next question is how do we detect a higher priority heading level?
Conditional statement to detect overlapping ranges
Instead of detecting the higher heading level directly, we just need to detect whether the ranges overlap. More specifically, we need to know if the “previous” heading range has expanded over our current heading range. The conceptual If statement logic to decide what to do is:
The Else part is the regular case for this macro. See this previous article if you want to review conditional statements in VBA.
Checking for one range inside another
All range variables include an InRange method. If we want to know whether the current heading range rCurrent is inside another range we reference the InRange method and give it a range to test:
This statement asks whether the rCurrent range is inside the rPrevious range. The result is a True or False (Boolean) value we can use in the more detailed conditional statement below.
Span the full previous heading range
If the ranges overlap we just want to span the full previous heading. We just redefine the current heading range to be the previous one.
This range includes the old “current” heading range, but it may extend beyond it in either or both directions. That was easier than all the hoopla above made it sound.
Span the extended range
Extending the range is similar to the earlier macro, but we instead need to set the Start position of our extended range to the Start of the previous heading range and the End position to the End of the current heading range. We again use the SetRange method and give it these Start and End positions.
Conditional statement
Putting the earlier conditional statement and the two range assignments together, our more specific conditional statement is:
If you don’t like this overlapping range check, the following alternative version based on outline levels after the macro.
Alternative heading level validation for range expansion
An alternative validation check for whether we need to extend or expand the current selection uses the outline level of the heading paragraph. Only one version is needed.
Technically, the earlier InRange method doesn’t actually validate whether we’ve expanded over the higher-level heading range. It just checks whether one range is inside the other. It shouldn’t be a problem in practice, but we can be more specific if we insist.
Define outline level variables
If we want to be more concrete, we can compare the actual heading (outline) levels before expanding our heading selection. It will be easier to read if we use a couple outline level variables.
Technically, these data types are defined using the WdOutlineLevel enumeration (an "enumeration" is just a list of constants with nicer names), but that is overcomplicating things. In the end, they are just outline level numbers 1 (highest priority heading level) through 9 (lowest priority heading level) with 10 corresponding to body text.
Store current heading level value
After selecting the current heading range, we can store the outline level of the first paragraph.
We’re referencing the First paragraph of the Paragraphs collection which is the heading paragraph. Every paragraph has its own OutlineLevel property which is just a numerical value between 1 and 10 as mentioned above, but since this command references the heading paragraph, we know we’re getting the outline level of the heading.
Store previous heading level value
Once we define our previous heading range, we can also store its outline level in the same manner.
Again, we’re referencing the first paragraph since it is the heading paragraph.
Outline level comparison
We set up a conditional statement where we literally just compare the two numbers using a less than < sign.
A lower number is a higher priority heading.
Conditional statement
If we find a higher priority outline level, we expand the range over the full previous heading. Our conditional statement is then:
The InRange version is more concise, but this version is more explicit. I prefer the more concise version in this macro.
Final extend to previous heading range macro
The “previous version” requires extra steps compared to the above version to properly check the extended or expanded heading range.
One could readily tweak these macros to create “shrink” versions, but in the interest of “brevity,” these are left to the reader. The differences are not significant.