Click here to Skip to main content
15,879,096 members
Articles / Programming Languages / Visual Basic
Article

Handy .NET Macro

Rate me:
Please Sign up or sign in to vote.
3.25/5 (10 votes)
16 Jan 20062 min read 47.1K   246   22   9
This macro has a handful of methods that I find useful with .NET.

Introduction

This macro holds helpful methods for use in Visual Studio .NET. For those of you not familiar with macro writing - if you don't know how to do something, just try recording a macro and you can usually view the auto-generated code and get some idea of where to start with your own code. The methods available in this macro are:

  1. CommentAllRoutines

    Inserts comments for all the routines in the current document. Gives creation date, author, parameters, and method names.

  2. CommentSelectedRoutines

    As above, but only comments the routines you have selected.

  3. IncrementTabIndexes

    I came up with this method after getting annoyed of having to manually reset the tab indexes of ASP web controls. Select your HTML, and the first tab index will set the counter, then subsequent tab indexes will be incremented by 1.

  4. IncrementUserText

    As above, but you get an input box so that you can specify your own field to search and increment.

  5. InsertModuleHeader

    Inserts the creation date, author, and copyright info at the top of your module.

  6. InsertProperties

    Select your class level variables (must be prefixed with “_”), and this method will automatically take care of the tedious stuff of writing properties for you.

  7. RegionToggle

    Select the top or bottom of a region, and this method will take you to the other end of the region.

  8. CloseAllOtherWindows

    This macro will close all the open windows except the one you're working on.

As far as I can tell, you can only write macros in VB.NET, and you can't debug them when you're working on a C# project.

Install instructions

Save the CustomMacros.vb file to somewhere, then open the macros explorer by opening Visual Studio and pressing Alt+F11, and choosing File-->Add Existing, and choosing the saved macro.

It's all pretty self explanatory. This code comes without any guarantee but if you use it, please reference that you got it off me. Any comments appreciated. I've created these macros through a combination of editing pre-existing stuff, using the sample macros, recording macros, and plain old trial and error. I apologize if I've accidentally ripped off someone's work without acknowledging them - if I have, it certainly wasn't intentional.

VB
Imports EnvDTE
Imports System.Diagnostics
Imports System.Text
Imports System.Environment
Imports System.Windows.Forms

Public Module CustomMacros

#Region "Class level variables"
     Private Const cCommentConst As String = "//"
    Private Const vbCommentConst As String = "'"
    Private Const ownerLineConst As String = _
            "This program and code is property" & _ 
            " of [enter your name here]"
    Private Const copyRightConst As String = _
            "© 1988 to 2006. All Rights Reserved"
    Private Const commentBorderConst As String = _
            " --------------------------------" & _ 
            "---------------------------------------------"
    Private counter As Integer

#End Region

#Region "Insert Module Header"
    ''' <summary>
    ''' Inserts module header at top of document
    ''' as well as the legal spiel
    ''' </summary>
    Sub InsertModuleHeader()
        Dim projitem As ProjectItem = DTE.ActiveDocument.ProjectItem
        Dim filecm As FileCodeModel = projitem.FileCodeModel
        Dim element As CodeElement = filecm.CodeElements.Item(1)
        Dim celttype As CodeType

        '' For the sample, don't bother recursively descending all code like
        '' the OutlineCode sample does. Just get a first CodeType in the
        '' file.
        If (TypeOf element Is CodeNamespace) Then
            element = element.members.item(1)
        End If
        If (TypeOf element Is CodeType) Then
            celttype = CType(element, CodeType)
        Else
            Throw New System.Exception("Didn't find a type" & _ 
                  " definition as first thing in file or find" & _ 
                  " a namespace as the first thing with" & _ 
                  " a type inside the namespace.")
        End If

        Dim edPoint As EditPoint = _
            element.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint()

        '' Make doc comment start.
        Dim commentStart As String = LineOrientedCommentStart()
        If (commentStart.Length = 2) Then
            commentStart = commentStart & commentStart.Chars(1) & " "
        ElseIf (commentStart.Length = 1) Then
            commentStart = commentStart & commentStart.Chars(0) & _
                           commentStart.Chars(0) & " "
        End If

        Try
            DTE.UndoContext.Open("Insert Module Header")
            If (element.Kind = vsCMElement.vsCMElementClass) Then
                ' Create comments
                'Set cursor to top of document 
                edPoint.StartOfDocument()
                edPoint.StartOfLine()
                edPoint.Insert(NewLine & NewLine)
                edPoint.LineUp()

                edPoint.Insert(commentStart & commentBorderConst & NewLine)
                edPoint.Insert(commentStart & element.Name & NewLine)
                edPoint.Insert(commentStart & NewLine)
                edPoint.Insert(commentStart & ownerLineConst & NewLine)
                edPoint.Insert(commentStart & copyRightConst & NewLine)
                edPoint.Insert(commentStart & NewLine)
                edPoint.Insert(commentStart & "Author:" & _
                        ControlChars.Tab & UserName & NewLine)
                edPoint.Insert(commentStart & "Date:" & _
                        ControlChars.Tab & _
                        System.DateTime.Now.ToLongDateString() & _
                        NewLine)
                edPoint.Insert(commentStart & commentBorderConst)
            End If
        Finally
            DTE.UndoContext.Close()
        End Try
    End Sub

#End Region

#Region "CommentRoutines"
     ''' <summary>
    ''' InsertDocComments for the selected procedures by calling 
    ''' commentallprocedures with commentall set to false
    ''' </summary>
    Public Sub CommentSelectedRoutines()
        CommentAllRoutines(False)
    End Sub

    ''' <summary>
    ''' InsertDocComments goes through the current
    ''' document using the VS Code Model
    ''' to add documentation style comments to each function.
    ''' </summary>
    Public Sub CommentAllRoutines(Optional ByVal _
                      commentAll As Boolean = True)
        Dim projItem As ProjectItem = DTE.ActiveDocument.ProjectItem
        Dim filecm As FileCodeModel = projItem.FileCodeModel
        Dim element As CodeElement = filecm.CodeElements.Item(1)
        Dim elementtype As CodeType
        Dim ts As TextSelection = ActiveDocument.Selection

        '' Get the first CodeType in the file.
        If (TypeOf element Is CodeNamespace) Then
            element = element.members.item(1)
        End If
        If (TypeOf element Is CodeType) Then
            elementtype = CType(element, CodeType)
        Else
            Throw New System.Exception("Type definition" & _ 
                  " or namespace definition" & _ 
                  " not found as first thing in file.")
        End If

        Dim edPoint As EditPoint = _
         elementtype.GetStartPoint(vsCMPart.vsCMPartHeader).CreateEditPoint()

        '' Make doc comment start.
        Dim commentStart As String = LineOrientedCommentStart()
        If (commentStart.Length = 2) Then
            commentStart = commentStart & commentStart.Chars(1) & " "
        ElseIf (commentStart.Length = 1) Then
            commentStart = commentStart & _
              commentStart.Chars(0) & _
              commentStart.Chars(0) & " "
        End If

        '' Make this atomic & Use Try...Finally to ensure Undo works
        Try
            If commentAll Then
                DTE.UndoContext.Open("Comment All Routines")
            Else
                DTE.UndoContext.Open("Comment Selected Routines")
            End If

            '' Iterate over code elements emitting doc comments for functions.
            For Each element In elementtype.Members
                If (element.Kind = vsCMElement.vsCMElementFunction) Then
                    '' Get Params.
                    Dim codefun As CodeFunction = element

                    edPoint.MoveToPoint(
                      codefun.GetStartPoint(vsCMPart.vsCMPartHeader))
                    If (edPoint.Line >= ts.TopPoint.Line And _
                        edPoint.Line <= ts.BottomPoint.Line) _
                        Or commentAll Then

                        Dim params As CodeElements = codefun.Parameters

                        'Insert comment
                        edPoint.Insert(NewLine)
                        edPoint.LineUp()

                        'Insert opening border
                        edPoint.Insert(ControlChars.Tab & commentStart & _
                                commentBorderConst & NewLine)

                        'Insert summary
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & "<summary>" & NewLine)
                        edPoint.Insert(ControlChars.Tab & commentStart & _
                                ControlChars.Tab & "Summary of " & _
                                element.Name & NewLine)
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & "</summary>")

                        Dim element2 As CodeElement
                        Dim cp As CodeParameter

                        'Insert parameters
                        For Each element2 In params
                            cp = element2
                            edPoint.Insert(NewLine)
                            edPoint.Insert(ControlChars.Tab & commentStart)
                            edPoint.Insert("<param name=" & _
                                    cp.Name & "></param>")
                        Next 'param

                        'Insert History
                        edPoint.Insert(NewLine)
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & "<history>" & NewLine)
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & ControlChars.Tab)
                        edPoint.Insert("[" & UserName & "]" & ControlChars.Tab)
                        edPoint.Insert(System.DateTime.Now.ToLongDateString() & _
                                ControlChars.Tab & "Created" & NewLine)
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & "</history>" & NewLine)

                        'Insert closing border
                        edPoint.Insert(ControlChars.Tab & _
                                commentStart & commentBorderConst)
                    End If
                End If 'we have a function
            Next 'code elt member
        Finally
            DTE.UndoContext.Close()
        End Try

    End Sub

#End Region

#Region "InsertProperties"
     ''' <summary>
    '''     Create properties for selected class level
    '''     variables. NB these variables should all be
    '''     prefixed with a "_" in order to avoid case 
    '''     sensitivity issues. Works for C# & VB.Net
    ''' </summary>
    Public Sub InsertProperties()

        Dim ts As TextSelection = DTE.ActiveWindow.Selection
        Dim propertyList(ts.TextRanges.Count - 1) As String
        Dim iIndex As Integer
        Dim startPoint As EditPoint = ts.TopPoint.CreateEditPoint()
        Dim endPoint As TextPoint = ts.BottomPoint

        Try
         Do While (startPoint.LessThan(endPoint))

          propertyList(iIndex) = _
           startPoint.GetText(startPoint.LineLength).Trim.Replace("New",_
           "").Replace("()", "")

          startPoint.LineDown()
          startPoint.StartOfLine()
          iIndex += 1
        Loop
        Catch ex As System.Exception
            Debug.WriteLine(ex)
        End Try

        Try
            DTE.UndoContext.Open("Insert Properties")

            For iIndex = 0 To propertyList.GetUpperBound(0)
                InsertProperty(propertyList(iIndex))
            Next
        Catch ex As System.Exception
            Debug.WriteLine(ex)
        Finally
            DTE.UndoContext.Close()
        End Try

    End Sub

    Private Sub InsertProperty(ByVal prop As String)

        If prop.Length = 0 Then Exit Sub

        Dim pType As String
        Dim pMember As String
        Dim pName As String
        Dim ts As TextSelection

        If LineOrientedCommentStart() = vbCommentConst Then
            'Select properties for vbProperties
            pMember = prop.Substring(prop.IndexOf(" "c)).Trim

            'Remove any value assignment
            If pMember.IndexOf("=") >= 0 Then
                pMember = Left(pMember, pMember.IndexOf("=")).Trim
            End If

            pType = pMember.Substring(pMember.IndexOf("As"))
            If pMember.StartsWith("_") Then
                pName = pMember.Substring(1)
            Else
                pName = pMember
            End If
            pName = pName.Substring(0, pName.IndexOf("As")).Trim
            pName = Left(pName, 1).ToUpper & pName.Substring(1)

            pMember = pMember.Substring(0, _
                      pMember.Length - pType.Length).Trim
        Else
            'Select properties for c#
            pType = prop.Substring(prop.IndexOf(" "c)).Trim

            'Remove any value assignment
            If pType.IndexOf("=") >= 0 Then
                pType = Left(pType, pType.IndexOf("=")).Trim
            End If

            pMember = pType.Substring(pType.IndexOf(" "c)).Trim
            pType = Left(pType, (pType.IndexOf(" "c))).Trim

            If pMember.StartsWith("_") Then
                pName = pMember.Substring(1)
            Else
                pName = pMember.Substring(0, 1).ToUpper & _
                        pMember.Substring(2)
            End If
        End If

        ts = DTE.ActiveWindow.Selection

        'Move cursor to two lines below selection
        ts.MoveToPoint(ts.BottomPoint)
        ts.NewLine(2)
        ts.StartOfLine()

        ts.Insert(GetPropertyString(pName, pType, pMember))

    End Sub

    Private Function GetPropertyString(ByVal propertyName _
            As String, ByVal propertyType As String, _
            ByVal propertyMember As String) As String
        Dim propertyString As New StringBuilder

        If LineOrientedCommentStart() = vbCommentConst Then
            'Return vbformat
            propertyString.Append(ControlChars.Tab & _
                 "Public Property " & propertyName & _
                 "() " & propertyType & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "Get" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "Return " & propertyMember & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "End Get" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "Set (ByVal Value " & _
                 propertyType & ")" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 propertyMember & "= Value" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "End Set" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 "End Property" & NewLine)
        Else
            'Return c# format
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "public " & _
                 propertyType & " " & propertyName & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "{" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "get" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "{" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 ControlChars.Tab & "return " & _
                 propertyMember & ";" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "}" & NewLine)

            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "set" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "{" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 ControlChars.Tab & propertyMember & _
                 "=Value;" & NewLine)
            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & ControlChars.Tab & _
                 "}" & NewLine)

            propertyString.Append(ControlChars.Tab & _
                 ControlChars.Tab & "}" & NewLine)
        End If

        Return propertyString.ToString
    End Function

#End Region

#Region "IncrementFields"
     ''' ------------------------------------------------------
    ''' <summary>
    ''' Loop through selected lines looking
    ''' for tabIndex=". The first number found after the
    ''' tabIndex sets the index. Subsequent matches
    ''' in the selected text will be incremented by 1.
    ''' </summary>
    ''' <param name="ds"></param>
    ''' <remarks>
    ''' </remarks>
    ''' <history>
    '''     [dgera]    05/01/2006    Created
    ''' </history>
    ''' ------------------------------------------------------
    Public Sub IncrementTabIndexes()
        IncrementLine("tabIndex=" & """")
    End Sub

    Public Sub IncrementUserText()
        Dim userText As String
        userText = InputBox("Enter the text to search" & _ 
                   " for and increment " & NewLine & _
                   "(e.g. tabIndex=" & """" & ")" & NewLine _
                   & NewLine & "n.b. searching is" & _ 
                   " case sensitive", "Custom Increment")

        If userText.Length > 0 Then
            IncrementLine(userText)
        End If
    End Sub

    ''' ------------------------------------------------------
    ''' <summary>
    ''' Loop through selected lines and call
    ''' GetIncrementedLine to increment the searchstring
    ''' </summary>
    ''' <param name="ds"></param>
    ''' <remarks>
    ''' </remarks>
    ''' <history>
    '''     [dgera]    05/01/2006    Created
    ''' </history>
    ''' ------------------------------------------------------
    Private Sub IncrementLine(ByVal searchString As String)

        Dim Insertion As New StringBuilder

        Dim StartLine, EndLine, Temp, numLines, i As Integer

        counter = -1

        Try
            DTE.UndoContext.Open("Increment Fields")

            StartLine = DTE.ActiveDocument().Selection.TopLine
            EndLine = DTE.ActiveDocument().Selection.BottomLine

            numLines = EndLine - StartLine

            If EndLine < StartLine Then
                Temp = StartLine
                StartLine = EndLine
                EndLine = Temp
            End If

            'Select each line and iterate. I know 
            'it would be more efficient to not select each line
            'but if you figure out how to do it please let me know
            For i = StartLine To EndLine
                ActiveDocument().Selection.GotoLine(i)
                ActiveDocument().Selection.SelectLine()
                Insertion.Append(GetIncrementedLine(
                   DTE.ActiveDocument().Selection.text(), _
                   searchString))
            Next

            'Set selection back to the first line
            ActiveDocument().Selection.GotoLine(StartLine)
            ActiveDocument().Selection.SelectLine()

            'Then select all the originally selected lines.
            'NB I tried to hold the original selection in
            'memory as a textSelection object but it works 
            'byRef so the above select statements 
            'caused it to change

            DTE.ActiveDocument().Selection.LineDown(True, numLines)

            'Delete selected lines and replace with new lines
            DTE.ActiveDocument.Selection.Delete()
            DTE.ActiveDocument.Selection.Insert(Insertion.ToString)

            'Set selection back to the top one last time
            ActiveDocument().Selection.GotoLine(StartLine)
            ActiveDocument().Selection.SelectLine()

            'now highlight the lines again 
            'to make it obvious to the user
            DTE.ActiveDocument().Selection.LineDown(True, _
                                                numLines)

        Catch ex As System.Threading.ThreadAbortException
            'Has been interrupted by the user so disregard
        Catch ex As System.Exception
            MessageBox.Show("An error has occurred" & _ 
              " in the IncrementLineMacro: - " & ex.Message)
        Finally
            DTE.UndoContext.Close()
        End Try

        counter = -1
    End Sub

    ''' -------------------------------------------
    ''' <summary>
    ''' Search line for the searchstring.
    ''' If not found return
    ''' as is else increment the integer
    ''' of searchstring and return
    ''' <summary>
    ''' <param name="ds"></param>
    ''' <remarks>
    ''' </remarks>
    ''' <history>
    '''     [dgera]    05/01/2006    Created
    ''' </history>
    ''' -------------------------------------------
    Private Function GetIncrementedLine(ByVal line As String, _
            ByVal searchString As String) As String
        Dim midString, returnString, numString, _
            leftString, rightString As String

        Dim startIndex As Integer = InStr(line, searchString)
        'Position of the start of the searchstring in the line

        Dim endIndex, endSearchString As Integer

        If startIndex > 0 Then
            endSearchString = startIndex + searchString.Length
            endIndex = InStr(endSearchString, line, _
                       """", CompareMethod.Text)

            numString = Mid(line, endSearchString, _
                        endIndex - endSearchString)

            If counter = -1 Then
                counter = Integer.Parse(numString)
            Else
                counter = counter + 1
            End If

            midString = searchString & counter & """"
            leftString = Left(line, startIndex - 1)
            rightString = Mid(line, endIndex + 1)

            returnString = leftString & _
                           midString & rightString
            Return returnString

        End If

        Return line
    End Function

#End Region

#Region "Region Toggle"
     ''' <summary>
    '''     Select the top or bottom of a region and
    '''     this method will take you
    '''     to the other end of the region
    ''' </summary>
    Public Sub RegionToggle()

        Dim regionSearch As String
        Dim searchUp As Boolean
        Dim ts As TextSelection = DTE.ActiveWindow.Selection
        ts.SelectLine()

        If ts.Text.IndexOf("#End Region") >= 0 Or _
                   ts.Text.IndexOf("endregion") >= 0 Then
            searchUp = True
        End If

        If searchUp Then
            ts.LineUp()
            ts.FindText("#", _
               vsFindOptions.vsFindOptionsBackwards)
        Else
            ts.FindText("#")
        End If

    End Sub

#End Region

#Region "Close all other windows"
     ''' <summary>
    '''     Leave the current window open and close all others
    ''' </summary>
    Public Sub CloseAllOtherWindows()

        Dim currentDoc As String

        currentDoc = DTE.ActiveDocument.Name()

        Dim doc As EnvDTE.Document

        For Each doc In DTE.Documents
            If doc.Name <> currentDoc Then
                doc.Close()
            End If
        Next

        DTE.Windows.Item(currentDoc).Activate()

    End Sub

#End Region

#Region "Common Helper Methods"
     ''' <summary>
    ''' Figures out if you're using vb.net or csharp
    ''' and returns the relevant comment string
    ''' </summary>
    Private Function LineOrientedCommentStart(Optional _
            ByVal doc As Document = Nothing) As String
        If (doc Is Nothing) Then
            doc = DTE.ActiveDocument
        End If

        Dim ext As String = doc.Name

        If (ext.EndsWith(".cs")) Then
            Return cCommentConst
        ElseIf (ext.EndsWith(".cpp")) Then
            Return cCommentConst
        ElseIf (ext.EndsWith(".h")) Then
            Return cCommentConst
        ElseIf (ext.EndsWith(".vb")) Then
            Return vbCommentConst
        ElseIf (ext.EndsWith(".idl")) Then
            Return cCommentConst
        End If
    End Function

    ''' <summary>
    ''' Figures out if you're using vb.net or csharp
    ''' </summary>
    Private Function IsCSharp() As Boolean
        Return (LineOrientedCommentStart() = cCommentConst)
    End Function

#End Region

End Module

Points of Interest

Creating macros is pretty easy to do once you get started - and the ability to record things can help enormously.

The only thing I'm not really happy with is in the incrementing routines. I am retrieving the text of each line by selecting each line. It would be better if I could select it all and split it using the carriage return character, but I found that it was prone to be buggy, and sometimes the split returned multiple lines. If anyone finds a way to improve on this, please let me know.

Cheers, Darryl Gera.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
United Kingdom United Kingdom
I've worked in IT for 9 years now - mostly in the Microsoft space but with a little bit of Java, Oracle and Unix for good measure.

I've worked in .Net since beta 1 and don't really care whether I use VB.Net or C# - they're both pretty good to work with and a lot easier to use than VB6 or C++.

I'm from New Zealand but am currently working as a contract consultant in the UK.

I'm trying to get around to sitting the MCSD exams but I keep getting too busy. I'll get there eventually though.

Comments and Discussions

 
GeneralMy vote of 3 Pin
TusharBharambe24-Apr-13 5:13
TusharBharambe24-Apr-13 5:13 
QuestionFunction end point Pin
princess sarah27-May-08 23:08
princess sarah27-May-08 23:08 
AnswerRe: Function end point Pin
dazza200412-Jun-08 3:51
dazza200412-Jun-08 3:51 
GeneralFunction comment Pin
princess sarah21-May-08 22:36
princess sarah21-May-08 22:36 
AnswerRe: Function comment Pin
dazza200421-May-08 23:59
dazza200421-May-08 23:59 
GeneralGreat Pin
Thomas Wells5-Oct-07 10:56
Thomas Wells5-Oct-07 10:56 
GeneralException in Visual Studio 2005 Pin
Manish Pansiniya9-Nov-06 0:06
Manish Pansiniya9-Nov-06 0:06 
GeneralRe: Exception in Visual Studio 2005 Pin
dazza20049-Nov-06 0:28
dazza20049-Nov-06 0:28 
GeneralNice Macros Pin
SlinkingFerret2-Feb-06 13:49
SlinkingFerret2-Feb-06 13:49 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.