Useful Code: Swap .h/.cpp

Over the past few weeks, for a number of reasons I have been working on a number of macros to extend the functionality of Visual C++. The first reason is that I have been giving preparing demos for various talks, such as Gamefest (which I mentioned earlier) as well as PDC. In these talks, we stress the many ways users can customize the IDE to suit their needs and extending it with macros is a key part of that. The second, more important reason, is that a number of customers have asked for features that we simply cannot add to the product at this point in the release cycle. When the developers on my team heard I was doing this, they even went so far as to say I was doing "their job". Needless to say I was quite flattered :) What's more, they started mentioning that they had a bunch of their own, which they'd been keeping to themselves this whole time!

 

Although this post is meant to discuss a specific macro (first among a few I'll be posting), I want to talk a bit about Visual Studio's extensibility model. If you've already written add-ins or macros then this may be old news, and I promise to keep it brief.

 

Writing macros for Visual Studio is quite simple (could you guess I was going to say that?). You can access the macros editor, which is simply a sub-session of VS, from the Tools menu, under the macros entry, or with the Alt+F11 shortcut. If it's the first time you do this, you should see a project called MyMacros with an empty module called Module1, which is a great starting point to write a macro. At this point, you will have inevitably noticed that you have entered the wonderful world of VB development where it seems as though everything is taken care of for you… The two core APIs for C++ oriented macro development are DTE, which provides access to the IDE functionality, and the VCCodeModel, which is the gateway to working with the code inside the IDE. As I unveil more macros, I'll expound more on these interfaces.

 

Today's macro is a feature request I've often received from customers: "Give me a shortcut to swap between a .h and respective .cpp file". Ask and (sometimes) ye shall receive so here is the code in all its simplistic glory. The comments should speak for themselves…

 

    Sub SwapHeaderImpl()

 

        ' get the currently active document from the IDE

        Dim doc As EnvDTE.Document = DTE.ActiveDocument

        ' get the name of the document (lower-case)

        Dim docname As String = doc.Name.ToLower

        ' get the project that contains this document

        Dim project As Project = doc.ProjectItem.ContainingProject

 

        ' verify that we are working with a C++ document

        If doc.Language = EnvDTE.Constants.dsCPP Then

 

            ' switch file name string between *.h <-> *.cpp

            If docname.EndsWith(".h") Then

                docname = docname.Replace(".h", ".cpp")

            ElseIf docname.EndsWith(".cpp") Then

                docname = docname.Replace(".cpp", ".h")

            End If

 

            ' find file in current project and open it (can you spot the flaw in this section?)

            Dim item As ProjectItem

            For Each item In project.ProjectItems

 

                ' compare and open

                If docname = item.Name.ToLower() Then

                    DTE.ItemOperations.OpenFile(item.FileNames(0), Constants.vsViewKindCode)

                    Exit Sub

                End If

 

            Next

 

            ' if file was not in project, search include paths

            Dim vcproj As VCProject = project.Object

            Dim config As VCConfiguration = vcproj.Configurations(1)

            Dim compiler As VCCLCompilerTool = config.Tools("VCCLCompilerTool")

            Dim path As String

            For Each path In compiler.FullIncludePath.Split(";")

                If My.Computer.FileSystem.FileExists(path + "\" + docname) Then

                    DTE.ItemOperations.OpenFile(path + "\" + docname, Constants.vsViewKindCode)

                End If

            Next

 

        End If

    End Sub

 

You probably noticed that I point a flaw in the code above. Indeed, the code above won't be useful as it does not traverse the entire list of files contained in a project. The problem lies in the fact that some items within a project are both an item and a container of more items (i.e. folders/filters). These items can be accessed both as a ProjectItem object and as a ProjectItems object. Furthermore, items can be deeply nested, so in order to reach every file in a project, we need a recursive function such as the following.

 

    Sub OpenInProjectItems(ByVal name As String, ByVal projItems As EnvDTE.ProjectItems)

        Dim projItem As EnvDTE.ProjectItem

        ' Find all ProjectItem objects in the given collection

        For Each projItem In projItems

            If name = projItem.Name.ToLower() Then

                DTE.ItemOperations.OpenFile(projItem.FileNames(0), Constants.vsViewKindCode)

                Exit Sub

            End If

            ' recurse to get deeply nested items

            OpenInProjectItems(name, projItem.ProjectItems)

        Next

    End Sub

We can now rewrite the original macro using this helper function.

    Sub SwapHeaderImpl()

 

        ' get the currently active document from the IDE

        Dim doc As EnvDTE.Document = DTE.ActiveDocument

        ' get the name of the document (lower-case)

        Dim docname As String = doc.Name.ToLower

        ' get the project that contains this document

        Dim project As Project = doc.ProjectItem.ContainingProject

 

        ' verify that we are working with a C++ document

        If doc.Language = EnvDTE.Constants.dsCPP Then

 

            ' switch file name string between *.h <-> *.cpp

            If docname.EndsWith(".h")

                docname = docname.Replace(".h", ".cpp")

            ElseIf docname.EndsWith(".cpp") Then

                docname = docname.Replace(".cpp", ".h")

            End If

 

            ' find file in current project and open it

            OpenInProjectItems(docname, project.ProjectItems)

 

            ' if file was not in project, search include paths

            Dim vcproj As VCProject = project.Object

            Dim config As VCConfiguration = vcproj.Configurations(1)

            Dim compiler As VCCLCompilerTool = config.Tools("VCCLCompilerTool")

            Dim path As String

            For Each path In compiler.FullIncludePath.Split(";")

                If My.Computer.FileSystem.FileExists(path + "\" + docname) Then

                    DTE.ItemOperations.OpenFile(path + "\" + docname, Constants.vsViewKindCode)

                End If

            Next

 

        End If

    End Sub

I should also discuss the lower part of the code where the macro attemps to reach files outside of the project. Often, C++ developers may be implementing headers that are not added to the project but that are in the project's include path. In order to support this common scenario, the macro digs into the VC automation engine, as evidenced by the use of VCProject, VCConfiguration and VCCLCompilerTool. Navigating these APIs is a little more difficult but there is a lot of functionality buried in them. In this case, I load the first configuration (ideally I should retrieve the active configuration) for the project and then load the object containing all the properties of the compiler build event. Within this object I find the FullIncludePath property, which I can split to obtain all possible locations accessible from this project.

Voila!