Chapter 10. Text-Editing Objects and Events

Much of what a programmer does during the workday (and work night) involves editing text. In fact, editing text is so much a part of programming that many a successful business has been built around creating a better Notepad, and the popularity of these editors has grown in direct proportion to the number of mundane tasks that they automate and the extent to which they can be customized. As you learned in Chapter 3, Microsoft® Visual Studio® 2005 boasts a first-class code editor, and the automation object model lets you take advantage of the editor's functionality to create all the editing features that would have been included had you been in charge of development.

Editor Windows

If you want to edit text in the integrated development environment (IDE), you need a document; if you have a document, you also have an editor window. In Visual Studio 2005, documents and editor windows are inseparable, so it pays to know a little about editor windows even if editing text in documents is your ultimate goal. Figure 10-1 gives a sneak preview of the editor windows of interest to us in this chapter.

The Window Object

There's not much to say about the Window object—it's just a short stop on the way to more specialized windows. Finding a window is straightforward: if you want the window that has the focus, the DTE.ActiveWindow property returns it to you. If you want some other window and you know its caption, use DTE.Windows.Item(<caption>). (Figure 10-1 shows the code for retrieving the Connect.cs and HTMLPage1.htm windows.)

After you have a Window object, the most important property for finding other windows is Object, which returns the corresponding TextWindow or HTMLWindow object for editor windows. If you don't know for certain which type the Object property holds, you'll have to check by using the TypeOf…Is (Microsoft Visual Basic®) or is (C#) keyword:

If TypeOf DTE.ActiveWindow.Object Is TextWindow Then
    §
End If
Editor windows

Figure 10-1. Editor windows

Of course, if you don't check and you use the wrong object, you'll receive an exception courtesy of the common language runtime (CLR).

The TextWindow and HTMLWindow Objects

The TextWindow and HTMLWindow objects represent the editor windows in the IDE. Each type offers a small set of properties that give you access to editor-window-specific features.

Table 10-1 lists the TextWindow properties. The two properties of note are ActivePane and Panes, which give you access to the panes in a given editor window.

Table 10-1. TextWindow Properties

Property

Description

ActivePane

Returns the TextPane object associated with the active pane.

DTE

Returns the top-level DTE object.

Panes

Returns a TextPanes collection containing the panes in the window.

Parent

Returns the parent Window object.

Selection

Returns the TextSelection object for the active pane. (It is equivalent to Parent.Selection.)

Essentially, an HTMLWindow object is just a TextWindow object—except when it isn't. (We'll go into more detail on that a little bit later.) Table 10-2 shows the HTMLWindow properties.

Table 10-2. HTMLWindow Properties

Property

Description

CurrentTab

Sets or returns the currently selected tab (HTML or Design)

CurrentTabObject

Returns a TextWindow object when the HTML tab is selected; returns an IHTMLDocument2 interface when the Design tab is selected

DTE

Returns the top-level DTE object

Parent

Returns the parent Window object

The CurrentTab property uses values from the EnvDTE.vsHTMLTabs enumeration: vsHTMLTabsSource when setting or returning the HTML tab and vsHTMLTabsDesign when setting or returning the Design tab. The CurrentTabObject property returns a TextWindow object when the HTML tab is selected, which is why we suggested earlier that an HTMLWindow is just a TextWindow in disguise. When the Design tab is selected, however, CurrentTabObject returns an mshtml.IHTMLDocument2 interface, which provides access to the Dynamic HTML (DHTML) object model of the underlying document. Be aware that the views offered by the Design tab and HTML tab aren't synchronized; changes in one view won't propagate to the other until you switch views. In practical terms, this means that you should use references only to the current view.

Note

To use the mshtml namespace, you need its primary interop assembly: Microsoft.mshtml.dll. You can find this assembly at Program FilesMicrosoft.NETPrimary Interop Assemblies. Add-in writers can add a reference to this assembly by browsing to it from the Add Reference dialog box; macro writers first need to copy the DLL file to the Visual Studio 2005 PublicAssemblies folder before they can access the assembly.

As you now know, it takes several steps to discover whether a text window hides inside an arbitrary window. If you think it would be nice to have a function that takes care of these steps for you, you're in luck:

Function GetTextWindow(ByVal win As Window) As TextWindow
    ' Description: Returns the TextWindow object for a given window,
    '              or Nothing if not a text window

    Dim txtWin As TextWindow = Nothing

    ' Check for TextWindow
    If TypeOf win.Object Is TextWindow Then
       txtWin = win.Object

    ' Otherwise, check for HTMLWindow, then TextWindow
    ElseIf TypeOf win.Object Is HTMLWindow Then
        Dim htmlWin As HTMLWindow = win.Object

        If htmlWin.CurrentTab = vsHTMLTabs.vsHTMLTabsSource Then
            txtWin = htmlWin.CurrentTabObject
        End If
    End If

    Return txtWin
End Function

The TextPane Object

The TextPane object represents a pane in an editor window. Every editor window can be split into two panes to allow you to juxtapose two locations in a text file. You can split the view manually either by double-clicking the splitter bar—the thin rectangle at the top of the scroll bar—or by dragging the splitter bar to the desired location. Afterward, you can make changes to the same document through either pane.

Finding TextPane Objects

The automation object model makes it easy to find TextPane objects if you already have a TextWindow object: just use the ActivePane property or iterate through the Panes collection until you find the TextPane you want. Unfortunately, the HTMLWindow object doesn't offer similar properties directly, so you first have to use logic like that found in the GetTextWindow function earlier to extract a TextWindow from an HTMLWindow.

An alternative way of retrieving a TextPane is through the TextSelection object. TextSelection has a TextPane property that returns the pane to which the selection belongs. (TextPane has an orthogonal property, Selection, that returns the TextSelection in the pane.) TextWindow and HTMLWindow both have a Selection property, as does Window, which means there's an indirect path to TextPane over which all window objects can travel. For most purposes, however, using a TextWindow to find a TextPane works just fine.

One pane-related question you might ask is whether a second pane is open in an editor window. The following code gives you the answer:

Function IsSecondPaneOpen(ByVal txtWin As TextWindow) As Boolean
    ' Description: Returns whether a second pane is open in a text window

    Return (txtWin.Panes.Count = 2)
End Function

The TextPanes collection returned by Panes has one TextPane object for each pane in the window, so its Count property returns 2 when a second pane is open.

Here's a more interesting problem—finding the top or bottom pane in a window. The problem would be intractable except for the fact that the bottom pane is always at index 1 of its TextPanes collection. Given that bit of information, here are two functions that return the appropriate pane:

Function GetTopPane(ByVal txtWin As TextWindow) As TextPane
    ' Description: Returns the top pane in the text window

    Dim txtPane As TextPane = Nothing

    If txtWin.Panes.Count = 1 Then
        ' Only one pane, so return it
        txtPane = txtWin.ActivePane
    Else
        ' Top pane is always index 2
        txtPane = txtWin.Panes.Item(2)
    End If

    Return txtPane
End Function

Function GetBottomPane(ByVal txtWin As TextWindow) As TextPane
    ' Description: Returns the bottom pane in a text window. Returns
    '              top pane if only one pane is open

    ' Bottom pane is always index 1
    Return txtWin.Panes.Item(1)
End Function

The ActivateTopPane and ActivateBottomPane macros included with the book's sample files let you test the previous code on live windows.

One last question you might have is which pane a given TextPane belongs to. At first, it might seem easy enough to compare the given TextPane with its corresponding member in the TextPanes collection, but for the reasons given in the Chapter 9 sidebar ("Is It What It Says It Is?") you can't compare TextPane references for equality. Fortunately, you can compare TextSelection references successfully, an operation that provides all the information you need to write the following functions:

Function IsTopPane(ByVal txtPane As TextPane) As Boolean
    ' Description: Returns whether the given TextPane is the top pane

    Dim result As Boolean = False

    If txtPane.Collection.Count = 1 Then
        result = True
    Else
        If txtPane.Selection Is txtPane.Collection.Item(2).Selection Then
            result = True
        End If
    End If

    Return result
End Function

Function IsBottomPane(ByVal txtPane As TextPane) As Boolean
    ' Description: Returns whether the given TextPane is the bottom pane

    Dim result As Boolean = False

    If txtPane.Collection.Count = 2 Then
        result = _
            (txtPane.Selection Is txtPane.Collection.Item(1).Selection)
    End If

    Return result
End Function

Documents

At the risk of stating the obvious (and possibly the painfully obvious), the Visual Studio 2005 text editor operates on documents. When you program, it's easy to think that you're typing in a file; you load source code from a file when you begin editing and you save the changes to a file when you finish, so it's natural to assume that all the time in between is spent working on a file. However, a file is something that exists on disk—the document you work with in the text editor is something less permanent but infinitely more malleable. This section introduces you to the two objects that capture these qualities of documents and make them available to you through automation: the Document and TextDocument objects.

The Document Object

The Document object serves as a general-purpose wrapper for text data; it provides methods and properties that give you high-level control over both the data and the windows in which that data appears.

Creating and Finding Documents

You can create a document programmatically by using methods of the ItemOperations object, which is covered in Chapter 7, and the ProjectItems object, which is covered in Chapter 8. For example, the ItemOperations.NewFile method, which corresponds to the New command on the File menu, lets you create a file that isn't associated with a particular project. The following macro shows how to create a text file by using the NewFile method:

Sub CreateNewTextFile()
    ' Description: Shows how to use the ItemOperations.NewFile method
    '              to create a new text file

    Dim Item As String = "GeneralText File"
    Dim Name As String = "MyTextFile"
    Dim ViewKind As String = Constants.vsViewKindPrimary
    Dim win As Window

    win = DTE.ItemOperations.NewFile(Item, Name, ViewKind)
End Sub

One peculiarity of the NewFile method's Name parameter is that it specifies the name of the new document indirectly. With an existing file, the document name and the window caption both correspond to the file name. With a document created by NewFile, however, the Name parameter serves as the caption of the new document's window only—the document acquires the name of the temporary file created by Visual Studio 2005 to store the new document. The "indirectly" part happens when you save the document; Visual Studio 2005 displays the Name value as the default name of the file to save.

You have three main ways of finding and retrieving an existing Document object: the DTE.Documents collection, the Window.Document property, and the DTE.ActiveDocument property. The DTE.Documents collection contains a reference to every open Document object. Just as with any other collection in the automation object model, you can iterate through the Document objects in the collection looking for the one you want, or, if you know the name of the document, you can retrieve it by using the Documents.Item method, like so:

Dim doc As Document = DTE.Documents.Item("MyFile.cs")

If you have a Window object, its Document property returns the associated Document object. Some of the tests for this chapter use the following macro to retrieve the Document object of a Window; if the window doesn't exist, the macro creates a new text file with the requested caption and returns its Document object:

Function GetDocument(ByVal caption As String) As Document
    ' Description: Retrieves the Document object associated with
    '              the specified window, or creates a text file in
    '              a new window and returns its Document object

    Dim win As Window

    Try
        win = DTE.Windows.Item(caption)
    Catch ex As System.Exception
        win = DTE.ItemOperations.NewFile("GeneralText File", caption)
    End Try

    Return win.Document
End Function

Managing Document Windows

The relationship of Document objects to windows is one-to-many; a window always has one associated Document, but a Document can be open in many windows. You can open a new window on a document by using the Document.NewWindow method, which works in the same way as the New Window command on the Window menu. Each of the windows associated with a particular document will have as its caption the document name followed by a colon (:) and the window number, for example, Connect.cs:1, Connect.cs:2, and so on. Because the windows have the same underlying data, changes in one window will appear in all other related windows.

Warning

Visual Basic files don't support Document.NewWindow, so they throw a "not implemented" exception when you call this method.

The ability to have multiple windows means that you won't find a Document.Parent property that returns the containing window. (Which window would it return?) Instead, you can find all the windows associated with a particular document by iterating through the Document.Windows collection, as shown by the following macro:

Sub ListDocumentWindows()
    ' Description: Lists all the windows associated with
    '              each open document

    Dim pane As OutputWindowPane = _
        GetOutputWindowPane("List Document Windows")

    pane.Clear()

    Dim doc As Document

    For Each doc In DTE.Documents
        Dim win As Window

        pane.OutputString(doc.Name & " windows:" & vbCrLf)

        For Each win In doc.Windows
            pane.OutputString("   " & win.Caption & vbCrLf)
        Next

        pane.OutputString(vbCrLf)
    Next
End Sub

You can find the active window for the Document object by using its ActiveWindow property, which returns the active window, if applicable, or the topmost window associated with the document if none of the document's windows is active.

Warning

The Document.ActiveWindow property has a bug—it always returns the first document window, regardless of which window has the focus.

Managing Document Changes

The coarsest means available to the Document object for managing changes is its ReadOnly property, which allows you to get or set the document's read-only state. Methods that modify a document's text throw an exception if the document is read-only, so it's worth checking the ReadOnly property before you make text changes.

You can undo and redo changes to a document by using the Document.Undo and Document.Redo methods, respectively. These two methods offer the same functionality as their Edit menu counterparts. The Undo and Redo methods both return a Boolean value indicating whether the operation took place.

Warning

You can call Document.Undo or Document.Redo on a read-only document as many times as you want to, so long as the corresponding undo or redo stack is empty; in such cases, the method returns False. The problem is that you can change the Document.ReadOnly property on the fly, which means you can have undoable (or redoable) changes in your document when you switch from read-write to read-only. If you call Undo or Redo on a nonempty undo or redo stack of a read-only document, you get an exception.

The Document.TextSelection property returns the TextSelection object associated with the active window, or the topmost window if none of the document's windows has the focus. You can use the TextSelection object's myriad editing methods and properties to automate just about any editing task. (You'll learn all about TextSelection objects in the upcoming section titled "The TextSelection Object.")

Saving and Closing Documents

The Document.Save method saves the document and optionally lets you choose the name and the location to save to. The Save method throws an exception if the location you specify doesn't already exist; if you give a correct location but no name, the Save method uses the current name of the document. (A bug in the Microsoft Visual C++® implementation causes the Save method to ignore any new file name that you give it.) If you want to save every open document in one call, use the SaveAll method of the Documents collection. What you gain in convenience you give up in control—you can't specify new names or locations for the files as you can with the Save method.

The Document.Close method closes a document and also lets you pass in a vsSaveChanges value that signals whether to save changes (vsSaveChanges), discard changes (vsSaveChangesNo), or let the user decide whether to save changes (vsSaveChangesPrompt, which is the default). The Documents collection has a corresponding CloseAll method that lets you close every document and also specify a vsSaveChanges value to apply to every document.

The Document.Saved property indicates whether the document has changes that haven't yet been saved—a value of False means that the document has unsaved changes, as indicated by an asterisk in the document window's title bar. Essentially, this property controls whether the IDE prompts you to save a document when the document is closed. You can write to this property, but be aware that you will lose any unsaved changes if you close a document after setting its Saved property to True.

The TextDocument Object

Whereas a Document object can represent any document in the IDE, the TextDocument object represents text documents only. You retrieve a TextDocument object by using the Document.Object method and passing in an empty string or a value of "TextDocument"; the method returns null or Nothing for nontext documents.

The most important TextDocument properties and methods are those related to the TextPoint, EditPoint, and TextSelection editing objects. The TextDocument.Selection property returns the text document's selection and behaves the same as the Document.Selection property. The StartPoint and EndPoint properties return TextPoint objects that mark the beginning and end, respectively, of the text document buffer. The CreateEditPoint method returns an EditPoint object at the location of the TextPoint passed into the method; passing in null or Nothing creates an EditPoint at the beginning of the document.

Note

It makes little sense to call TextDocument.CreateEditPoint with a TextPoint parameter because a TextPoint object already has its own CreateEditPoint method. However, passing null to TextDocument.CreateEditPoint is the only way to create an EditPoint without first creating an intermediary point object.

Point Objects

As you might guess, a point object represents a position in a text document. The automation object model gives you three point objects to choose from: TextPoint, VirtualPoint, and EditPoint.

The TextPoint Object

The TextPoint object embodies the fundamental attributes of a text document location; VirtualPoint and EditPoint implement the TextPoint interface, so all point objects have these fundamental attributes in common. The following list gives you an idea of what these attributes might be:

  • Line information The Line property returns the number of the line that contains the point.

  • Offset information The AbsoluteCharOffset and LineCharOffset properties return the number of characters between the point and the beginning of the document, and between the point and the beginning of the current line, respectively.

  • Extreme information The AtStartOfDocument, AtEndOfDocument, AtStartOfLine, and AtEndOfLine properties allow you to determine whether the point is at the beginning or end of a document or line.

  • Relational information The LessThan, EqualTo, and GreaterThan methods let you discover the relation of one point with respect to another.

The TextPoint object doesn't have methods that allow you to edit text directly. Instead, you either pass these point objects to editing methods or use them to create an EditPoint object at a particular location, which you can then use to edit text. Table 10-3 shows you the different ways to find a TextPoint object.

Table 10-3. How to Retrieve a TextPoint Object

Returned By

Applies To

StartPoint property

TextDocument

TextPane

TextRange

EndPoint property

TextDocument

TextRange

The VirtualPoint Object

A VirtualPoint object represents a point in virtual space, which is a text editor feature that allows the insertion point to move indefinitely past the end of a line. When you type a character at a point in virtual space, the editor automatically fills in the space between the current end of the line and the new character. You can enable virtual space for all languages by opening the Tools—Options dialog box, selecting Text Editor—All Languages—General, and selecting the Enable Virtual Space check box in the Settings area. (See Figure 10-2.)

Enabling virtual space

Figure 10-2. Enabling virtual space

Table 10-4 shows the different ways you can find a VirtualPoint object. As you can see from the table, VirtualPoint objects spring from TextSelection objects, which gives you a clue to their function; selections can extend into virtual space, so the TextSelection object needs VirtualPoint objects to keep track of endpoints that fall outside the text buffer.

Table 10-4. How to Retrieve a VirtualPoint Object

Returned By

Applies To

ActivePoint property

TextSelection

AnchorPoint property

 

BottomPoint property

 

TopPoint property

 

As with the TextPoint object, one of the VirtualPoint object's main uses involves the creation of EditPoint objects. Be aware, however, that a VirtualPoint object can't create an EditPoint object in virtual space—if you try, the EditPoint object gets created at the end of the current line instead. You can avoid those situations by using the following function, which tells you when a VirtualPoint object has strayed into virtual space:

Function IsVirtualSpace(ByVal vrtPoint As VirtualPoint) As Boolean
    ' Description: Returns whether the VirtualPoint lies in virtual space

    Return vrtPoint.LineCharOffset <> vrtPoint.VirtualCharOffset
End Function

The VirtualPoint object defines a property named VirtualCharOffset that returns the distance between the point and the beginning of the line. The VirtualCharOffset property always has the same value as the LineCharOffset property, except when the point is in virtual space.

The EditPoint Object

The EditPoint is the workhorse of the point objects. In addition to the TextPoint methods and properties, EditPoint has methods that let you automate every possible modification of the text buffer. Table 10-5 shows the different ways you can retrieve an EditPoint object.

Table 10-5. How to Retrieve an EditPoint Object

Returned By

Applies To

CreateEditPoint method

 
 

EditPoint

 

TextPoint

 

VirtualPoint

 

TextDocument

We'll examine the EditPoint object's methods shortly, in the section titled "A Comparison of the TextSelection and EditPoint objects."

The TextSelection Object

The TextSelection object pulls double duty as a representation of the caret in the editor window as well as a representation of the currently selected text. (You can think of the caret as a zero-length selection.) Because there can be only one selection in an editor window, there can be only one TextSelection object per document. Figure 10-3 breaks down a TextSelection into its constituent parts.

As you can see in Figure 10-3, four properties delineate a TextSelection: TopPoint, BottomPoint, AnchorPoint, and ActivePoint. Each of these properties returns a VirtualPoint object from one of the ends of the selected range. The TopPoint and BottomPoint properties always refer to the upper left and bottom right of the selection, respectively. The AnchorPoint and ActivePoint properties refer to the equivalent of the starting point and the endpoint of a mouse-drag selection; for example, the top selection in Figure 10-3 would result from dragging the mouse from the beginning of using Extensibility; to the end of using EnvDTE;. You can determine the orientation of a TextSelection by checking its IsActiveEndGreater property, which returns True when ActivePoint equals BottomPoint. If the orientation isn't to your liking, you can flip it by calling the TextSelection.SwapAnchor method, which exchanges the positions of the AnchorPoint and ActivePoint objects.

Anatomy of a TextSelection object

Figure 10-3. Anatomy of a TextSelection object

The TextSelection.IsEmpty property lets you know whether there's a selection, and you can retrieve the selected text from the Text property. If there's no selection, Text always returns an empty string. The converse doesn't hold, however, because Text returns an empty string for a virtual space selection. When a selection spans multiple lines, the TextRanges property holds a collection of TextRange objects, one for each line of the selection.

Table 10-6 lists the different ways you can retrieve a TextSelection object.

Table 10-6. Properties That Return a TextSelection Object

Property

Applies To

Selection

Document

TextDocument

TextPane and TextPane2

TextWindow

Window and Window2

A Comparison of the TextSelection and EditPoint Objects

The TextSelection and EditPoint objects offer a bewildering array of editing methods, which are listed in Table 10-7. Looking at the table, you'll see that TextSelection and EditPoint share the majority of their methods and have only a few seemingly minor differences, which makes choosing one over the other akin to choosing between the 52-feature Swiss Army knife that comes with scissors and the 52-feature Swiss Army knife that comes with a saw. In most circumstances, either knife will do just fine—it's only in those particular moments when you need to gather firewood or do a little personal grooming that you suddenly realize that you can't cut down branches with scissors and you can't trim nose hairs with a saw. Using the editing objects is much the same in that you won't know whether you've chosen the right one for the job until it fails you.

Table 10-7. TextSelection and EditPoint Methods

Task

Methods in Common

TextSelection Only

EditPoint Only

Moving the insertion point

CharLeft, CharRight, EndOfDocument, EndOfLine, LineDown, LineUp, MoveToAbsoluteOffset, MoveToLineAndOffset, MoveToPoint, StartOfDocument, StartOfLine, WordLeft, WordRight

Collapse, GoToLine, MoveToDisplayColumn, PageDown, PageUp

 

Finding and retrieving text

FindPattern

FindText

GetLines, GetText

Selecting text

 

SelectAll, SelectLine

 

Modifying text

ChangeCase, Copy, Cut, Delete, DeleteWhitespace, Indent, Insert, InsertFromFile, PadToColumn, Paste, ReplacePattern, SmartFormat, Unindent

DeleteLeft, DestructiveInsert, NewLine, Tabify, Untabify

InsertNewLine[a], ReplaceText

Managing bookmarks

ClearBookmark, NextBookmark, PreviousBookmark, SetBookmark

  

Miscellaneous

OutlineSection

SwapAnchor

ReadOnly

[a] Defined by EditPoint2

The fundamental difference between the two objects is that the TextSelection object is view-based and the EditPoint object is buffer-based. The TextSelection object exists primarily to model user actions within the text editor—if you can do it by hand in the editor, you can do it with the TextSelection object. (You can see this demonstrated every time you record a macro: the Macro Recorder translates changes that you make to text documents into sequences of TextSelection statements.) This emphasis on WYSIWYG functionality, however, means that the global view state can affect the behavior of a TextSelection method. For example, when line wrapping is enabled, you can't count on TextSelection.LineDown to move the insertion point to the next line of text in the buffer—if the line wraps underneath the insertion point, moving the insertion point to the next line in the view serves only to move the insertion point farther down the same line in the buffer.

The EditPoint object, on the other hand, bypasses the view and operates on the buffer directly. Therefore, a call to EditPoint.LineDown always moves the EditPoint to the next line in the buffer regardless of the line-wrapping state. The only drawback of this insulation from the view is that you can't use EditPoint objects to affect virtual space.

So there you have it—if you want your add-ins and macros to make use of the view state automatically, use TextSelection. If you want complete control over the text buffer, use EditPoint.

Undo Contexts

The modern user interface has come a long way toward fulfilling one of humankind's greatest hopes—to be saved from itself. The undo facility you find in most of today's applications represents the crowning achievement of this pursuit. The next best thing to a time machine, undo allows you to roll back your most recent mistakes—usually with considerable relief—so that you can start making new ones in their place. The automation object model gives you full access to the Visual Studio 2005 undo manager, allowing you to select your own sets of mistakes that can be undone at the click of a mouse.

Automatic Undo Contexts

The basic unit of "undoability" is the undo context. (We'll use this term to mean both an undoable unit—the named entity that appears on the undo list—and the mechanism by which you group individual actions to create an undoable unit.) The Visual Studio 2005 IDE creates undo contexts automatically as you program, allowing you to undo and redo edits to your code. Try the following experiment to see some of the automatic undo contexts created by Visual Studio 2005:

  1. Open a blank text file in Visual Studio 2005.

  2. Type spelled backwards is epyT and press Enter.

  3. Copy a block of text from some document, and paste it into the text file.

When you've finished, click the Undo button's drop-down list and you'll see the list of undo contexts shown in Figure 10-4. (The drop-down list represents the document's undo stack, which is the internal data structure that stores the undoable changes.) The three undo contexts named Paste, Enter, and Type each represent one or more individual actions that can be undone as a whole. You can appreciate the ability to group multiple actions under a single name when it comes to large paste operations, because the alternative would be undoing the pasted characters one by one.

A list of undo contexts

Figure 10-4. A list of undo contexts

Creating Undo Contexts

An undo context is an atomic transaction; you open the undo context and give it a name, make changes to one or more documents, and then either commit the changes by closing the undo context or abort all the changes. Once committed, the changes can be undone as a group only. You create your own undo contexts by calling methods of the DTE.UndoContext object: Open begins an undo context, SetAborted discards all changes made within the current undo context, and Close commits the changes and pushes the undo context onto the undo stacks of the participating documents.

The undo manager in Visual Studio 2005 allows only one undo context at a time to be open, and to share that undo context, you must follow a few rules. First, always call Open within a try block because this method throws an exception if an undo context is already open. Although you can check the availability of the undo context by using the UndoContext.IsOpen property, which returns True when an undo context is open, a False value won't guarantee that the undo context will still be free by the time your code executes Open. Second, if you open an undo context, you should close it when you're finished with it by calling Close or SetAbort. (Use just one or the other because SetAbort closes the undo context for you, and calling Close on a closed undo context raises an exception.) Third, you should never call SetAbort or Close on someone else's undo context.

Because only one undo context can be open at a time, if you don't acquire the undo context, any changes you make will belong to some other context. If the changes you need to make absolutely must be in their own context, you'll have to poll the UndoContext.IsOpen property until the undo context becomes free.

Stack Linkage

Sooner or later, when you edit multiple documents within the same undo context, you will run across the problem of desynchronized undo stacks. Suppose you edit Document1 and Document2 within the Link undo context. After you close Link, it gets pushed onto the tops of the two documents' undo stacks. Then, if you undo Link in Document1, you also undo Link in Document2 because their edits belong to the same atomic operation. So far, so good.

Suppose you add some text to Document2. These new edits get pushed onto the top of Document2's undo stack. What happens now when you try to undo Link in Document1? To respect Link's atomicity, you have to undo Link in Document2, and there's the problem—you can't undo Link in Document2 without first undoing the text that was just added. The undo stacks have become desynchronized.

The undo manager solves this synchronization problem by introducing the concept of stack linkage. By default, an undo context that involves more than one document has a nonstrict stack linkage, which allows the atomicity of the undo context to be broken across documents; when the break happens, each document ends up with its own undo context containing only changes to itself. In our previous example, if the Link undo context were created with a nonstrict stack linkage, you could undo Link in Document1 without affecting Document2. Link would disappear from Document1's undo stack but remain on Document2's undo stack, minus the changes to Document1. A strict stack linkage, on the other hand, enforces the undo context's atomicity. If our previous example were to involve a strict stack linkage, the undo manager would cancel any attempt to undo Link in Document1.

You specify whether the stack linkage is strict through the second parameter to UndoContext.Open, passing True for strict. You can identify undo contexts with strict stack linkage by the plus (+) sign that precedes their names on undo lists.

Text Editor Events

Whereas text-editing objects tell you what you can do within the editor windows, text editor events tell you when you can do it. The automation object model defines several events that allow you to monitor editor window activities and take action based on what you find out. Together with text editor objects, text editor events allow you to achieve hands-free control over every important aspect of the editor windows.

The BeforeKeyPress and AfterKeyPress Events

The first two text editor events—BeforeKeyPress and AfterKeyPress—are new to Visual Studio 2005. Both of these events allow you to examine and act on the user's keystrokes in real time, which makes possible a vast selection of dynamic editing features.

The BeforeKeyPress and AfterKeyPress events have three parameters in common: KeyPress, Selection, and InStatementCompletion. The KeyPress parameter give you a string representation of the character that fired the event. The keypress events fire in response to any alphanumeric characters that the user types; in addition, the events fire for the Backspace, Delete, Space, Tab, and Enter keys, and Ctrl+Enter key combination.

Warning

The keypress events don't fire for just keyboard input—they also fire once for each character added through the TextSelection.Text property. If you're tempted to use the TextSelection.Text property inside a keypress event handler, take care to guard against a runaway recursion.

The Selection parameter gives you a reference to the active document's TextSelection object, which you can use to determine the location of the keypress and whether the keypress replaces existing text. Finally, the InStatementCompletion parameter warns you when the keypress coincides with an IntelliSense® statement completion.

In addition to the aforementioned event parameters, the BeforeKeyPress event has a CancelKeypress parameter. As you might guess, setting this parameter to True stops the keypress from ever reaching the editor window (and, consequently, prevents the AfterKeyPress event from firing).

To get a feel for how these keypress events work, Example 10-1 shows how to implement keypress event handlers as macros.

Example 10-1. Handling the BeforeKeyPress and AfterKeyPress events

<System.ContextStaticAttribute()> Public WithEvents _
    TextDocumentKeyPressEvents As EnvDTE80.TextDocumentKeyPressEvents

Private DTE2 As EnvDTE80.DTE2 = CType(DTE, EnvDTE80.DTE2)
Private output As OutputWindowPane

Public Sub  EnableKeyPressEventMacros()
    ' Description: Creates an Output window pane and initializes
    '              the TextDocumentKeyPressEvents variable.

    Try

        output = _
            DTE2.ToolWindows.OutputWindow.OutputWindowPanes.Item( _
            "KeyPress Events")
    Catch ex As System.Exception
        output = _
            DTE2.ToolWindows.OutputWindow.OutputWindowPanes.Add( _
            "KeyPress Events")
    Finally
        output.Activate()
    End Try

    TextDocumentKeyPressEvents = _
        DTE2.Events.GetObject("TextDocumentKeyPressEvents")
End Sub

Public Sub DisableKeyPressEventMacros()
    ' Description: Clears the Output window pane and resets the
    '              TextDocumentKeyPressEvents variable.

    TextDocumentKeyPressEvents = Nothing

    If Not IsNothing(output) Then
        output.Clear()
        output = Nothing
    End If
End Sub

Private Sub TextDocumentKeyPressEvents_BeforeKeyPress( _
    ByVal Keypress As String, _
    ByVal Selection As EnvDTE.TextSelection, _
    ByVal InStatementCompletion As Boolean, _
    ByRef CancelKeypress As Boolean)  _
    Handles TextDocumentKeyPressEvents.BeforeKeyPress
    ' Description: Handles the BeforeKeyPress event by displaying
    '              the event's parameters.

    output.OutputString("BeforeKeyPress: ")
    output.OutputString("KeyPress = " & Keypress & ", ")
    output.OutputString("Selection = '" & Selection.Text & "', ")
    output.OutputString("InStatementCompletion = " & _
        InStatementCompletion.ToString)
    output.OutputString(vbCrLf)

    If KeyPress.ToUpper() = "W" Then
        CancelKeypress = True
        output.OutputString(vbCrLf)
    End If
End Sub

Private Sub TextDocumentKeyPressEvents_AfterKeyPress( _
    ByVal Keypress As String, _
    ByVal Selection As EnvDTE.TextSelection, _
    ByVal InStatementCompletion As Boolean) _
    Handles TextDocumentKeyPressEvents.AfterKeyPress
    ' Description: Handles the AfterKeyPress event by displaying
    '              the event's parameters.

    output.OutputString("AfterKeyPress: ")
    output.OutputString("KeyPress = " & Keypress & ", ")
    output.OutputString("Selection = '" & Selection.Text & "', ")
    output.OutputString("InStatementCompletion = " & _
        InStatementCompletion.ToString)
    output.OutputString(vbCrLf & vbCrLf)
End Sub

If you run the EnableKeyPressEventMacros in Example 10-1 and start typing in an editor window, you'll see the values of the BeforeKeyPress and AfterKeyPress event parameters displayed in the Output window. To show how CancelKeypress might be used to make mischief, the BeforeKeyPress event handler effectively removes the W key without having to pry it loose from the keyboard.

The LineChanged Event

The automation object model defines a third event specific to editing: the LineChanged event. If your application needs only to process text line-by-line, sampling every keystroke might be overkill. In those instances, the LineChanged event offers a low-overhead alternative to the keystroke events.

The LineChanged event has three parameters to help you figure out why the event fired. The first two parameters, StartPoint and EndPoint, mark the beginning and end of the changes to the text buffer. You can use these TextPoint values to retrieve the changes, like so:

Dim text As String
text = StartPoint.CreateEditPoint.GetText(EndPoint)

The third parameter, Hint, is a bit flag that holds values from the vsTextChanged enumeration (shown in Table 10-8). The flags set in Hint are evidence that you can piece together to recreate the actions leading up to the event. (In practice, the Hint parameter doesn't give you quite enough information to figure out exactly what led to the event—but then, if it did, it wouldn't be called a hint.)

Table 10-8. The vsTextChanged Enumeration

Field

Description

vsTextChangedMultiLine

The changes affected multiple lines of text.

vsTextChangedSave

The changes were saved to disk.

vsTextChangedCaretMoved

The insertion point moved off the line containing the changes.

vsTextChangedReplaceAll

The entire text buffer was replaced by an insertion.

vsTextChangedNewLine

A new line was entered.

vsTextChangedFindStarting

A find operation moved the insertion point off the line containing changes.

The LineChanged event doesn't really fire when the line changes—that is, it doesn't fire for each new character added to or deleted from a line. Instead, the event fires when changes to a line are committed in some way, such as when the insertion point moves off the line, changes are saved to disk, or the document window loses focus. An undo context effectively disables this event until the undo context closes; afterward, the event fires if any of the changes made within the undo context would have caused it to fire under normal circumstances (the insertion point moves off a changed line, the entire text buffer is replaced by an insert, and so forth). The event handler receives StartPoint and EndPoint values that reflect all uncommitted changes from before and during the undo context.

Looking Ahead

In this book, we've presented a range of topics related to the use and customization of Visual Studio 2005. As you explore this amazing tool and the automation object model, you'll probably start to see completely new and exciting ways that you can customize and automate the IDE. We sincerely hope that we'll start seeing solutions that have in some way been helped along by the ideas and topics discussed here.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset