Supporting IntelliSense Member Completion (Managed Package Framework)

The IntelliSense Member Completion is a tool tip that displays a list of possible members of a particular scope such as a class, structure, enumeration, or namespace. For example, in C#, if the user types "this" followed by a period, a list of all members of the class or structure at the current scope is presented in a list from which the user can select.

The managed package framework (MPF) provides support for the tool tip and managing the list in the tool tip; all that is needed is cooperation from the parser to supply the data that appears in the list.

How It Works

The following are the two ways in which a member list is shown using the MPF classes:

  • Positioning the caret on an identifier or after a member completion character and selecting List Members from the IntelliSense menu.

  • The IScanner scanner detects a member completion character and sets a token trigger of MemberSelect for that character.

A member completion character is used to indicate a reference to a member of a class, structure, or enumeration. For example, in C# or Visual Basic, the member completion character is a "."; in C++, the character is either a "." or a "->" (known as the indirection operator). This trigger value is set when the member select character is typed and scanned, thus starting the member completion process.

The IntelliSense Member List Command

The SHOWMEMBERLIST command initiates a call to the Completion method on the Source class and the Completion method, in turn, calls the ParseSource method parser with the parse reason of DisplayMemberList.

The parser determines the context of the current position as well as the token under or immediately before the current position. Based on this token, a list of declarations is presented. For example, in C#, if you position the caret on a class member and select List Members, you get a list of all members of the class. If you position the caret after a period that follows an object variable, you get a list of all members of the class that object represents. Note that if the caret is positioned on a member when the member list is shown, selecting a member from the list replaces the member the caret is on with the one in the list.

The Token Trigger

The MemberSelect trigger initiates a call to the Completion method on the Source class and the Completion method, in turn, calls the parser with the parse reason of MemberSelect (if the token trigger also included the MatchBraces flag, the parse reason is MemberSelectAndHighlightBraces which combines member selection and brace highlighting).

The parser determines the context of the current position as well as what has been typed before the member select character. From this information, the parser creates a list of all members of the requested scope. This list of declarations is stored in the AuthoringScope object that is returned from the ParseSource method. If any declarations are returned, the member completion tool tip is displayed. The tool tip is managed by an instance of the CompletionSet class.

Enabling Support for Member Completion

You must have the CodeSense registry entry set to 1 to support any IntelliSense operation. This registry entry can be set with a named parameter passed to the ProvideLanguageServiceAttribute user attribute associated with the language package. The language service classes read the value of this registry entry from the EnableCodeSense property on the LanguagePreferences class.

If your scanner returns the token trigger of MemberSelect, and your parser returns a list of declarations, then the member completion list is displayed.

Supporting Member Completion in the Scanner

The scanner must be able to detect a member completion character and set the token trigger of MemberSelect when that character is parsed.

Example

Here is a simplified example of detecting the member completion character and setting the appropriate TokenTriggers flag. This example is for illustrative purposes only and does not reflect how a proper scanner should be written. Ideally, the detection for the member completion character would occur only when it was logical for such a character to appear at that particular place in the source file; for example, immediately following an identifier. This example uses brute force and sets the trigger for all member select characters, letting the parser handle the details.

using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;

namespace MyLanguagePackage
{
    public class MyScanner : IScanner
    {
        private const char memberSelectChar = '.';

        private string m_line;   // Line of text to parse.
        private int    m_offset; // Where next token starts in line.

        public void SetSource(string source, int offset)
        {
            m_line   = source;
            m_offset = offset;
        }

        public bool ScanTokenAndProvideInfoAboutIt(TokenInfo tokenInfo,
                                                   ref int state)
        {
            bool fFoundToken = GetNextToken(m_offset, tokenInfo, ref state);
            if (fFoundToken)
            {
                m_offset = tokenInfo.EndIndex + 1;
            }
            return fFoundToken;
        }

        private bool GetNextToken(int startIndex, TokenInfo tokenInfo, ref int state)
        {
            bool bFoundToken = false;      // Assume we are done with this line.
            int  index       = startIndex;

            if (index < m_line.Length)
            {
                bFoundToken = true;            // We are not done with this line.
                tokenInfo.StartIndex = index;
                char c = m_line[index];

                if (Char.IsPunctuation(c))
                {
                    tokenInfo.Type     = TokenType.Operator;
                    tokenInfo.Color    = TokenColor.Keyword;
                    tokenInfo.EndIndex = index;
                    if (c == memberSelectChar)
                    {
                        tokenInfo.Trigger |= TokenTriggers.MemberSelect;
                    }
                }
                else if (Char.IsWhiteSpace(c))
                {
                    do
                    {
                        ++index;
                    }
                    while(index < m_line.Length &&
                          Char.IsWhiteSpace(m_line[index]));
                    tokenInfo.Type     = TokenType.WhiteSpace;
                    tokenInfo.Color    = TokenColor.Text;
                    tokenInfo.EndIndex = index - 1;
                }
                else
                {
                    do
                    {
                        ++index;
                    }
                    while(index < m_line.Length &&
                          !Char.IsPunctuation(m_line[index]) &&
                          !Char.IsWhiteSpace(m_line[index]));
                    tokenInfo.Type     = TokenType.Identifier;
                    tokenInfo.Color    = TokenColor.Identifier;
                    tokenInfo.EndIndex = index - 1;
                }
            }
            return bFoundToken;
        }
    }
}

Supporting Member Completion in the Parser

Since the implementation of the parser is entirely up to the developer of the language, only general guidelines can be given here as to how to use the AuthoringScope class. For member completion, the Source class calls the GetDeclarations method on the AuthoringScope class to obtain a list of declarations. How you implement this list of declarations is up to you, but you must implement the list in a class that is derived from the Declarations class; the methods of the Declarations class access the list one element at a time, and thereby hide the details of the list's implementation. See the Declarations class for details on the methods you must implement.

The parser is called with the parse reason value of MemberSelect or MemberSelectAndHighlightBraces when the member select character such as a period is typed. The location given in the ParseRequest object is immediately after the member select character. The parser must collect the names of all members that can appear in a member list at that particular point in the source code. Then the parser must parse the current line to determine the scope the user wants associated with the member select character. This scope is typically based on the type of the identifier before the member select character.

For example, in C#, given the member variable languageService that has a type of LanguageService, typing "languageService." produces a list of all members on the LanguageService class. For another example in C#, typing "this." produces a list of all members on the class at the current scope.

Example

Here is one way to implement and populate a Declarations list. The assumption here is the parser puts only those declarations in the scope object that can be displayed in the members list by calling the custom AddDeclaration method on the MyAuthoringScope class.

using System.Collections;
using Microsoft.VisualStudio.Package;
using Microsoft.VisualStudio.TextManager.Interop;

namespace MyLanguagePackage
{
    internal class MyDeclaration
    {
        public string Name;            // Name of declaration
        public int     TypeImageIndex; // Glyph index
        public string Description;     // Description of declaration

        public MyDeclaration(string name, int typeImageIndex, string description)
        {
            this.Name = name;
            this.TypeImageIndex = typeImageIndex;
            this.Description = description;
        }
    }


    //===================================================
    internal class MyDeclarations : Declarations
    {
        private ArrayList declarations;

        public MyDeclarations()
            : base()
        {
            declarations = new ArrayList();
        }

        public void AddDeclaration(MyDeclaration declaration)
        {
            declarations.Add(declaration);
        }

        //////////////////////////////////////////////////////////////////////
        // Declarations of class methods that must be implemented.
        public override int GetCount()
        {
            // Return the number of declarations to show.
            return declarations.Count;
        }

        public override string GetDescription(int index)
        {
            // Return the description of the specified item.
            string description = "";
            if (index >= 0 && index < declarations.Count)
            {
                description = ((MyDeclaration)declarations[index]).Description;
            }
            return description;
        }

        public override string GetDisplayText(int index)
        {
            // Determine what is displayed in the tool tip list.
            string text = null;
            if (index >= 0 && index < declarations.Count)
            {
                text = ((MyDeclaration)declarations[index]).Name;
            }
            return text;
        }

        public override int GetGlyph(int index)
        {
            // Return index of image to display next to the display text.
            int imageIndex = -1;
            if (index >= 0 && index < declarations.Count)
            {
                imageIndex = ((MyDeclaration)declarations[index]).TypeImageIndex;
            }
            return imageIndex;
        }

        public override string GetName(int index)
        {
            string name = null;
            if (index >= 0 && index < declarations.Count)
            {
                name = ((MyDeclaration)declarations[index]).Name;
            }
            return name;
        }
    }


    //===================================================
    public class MyAuthoringScope : AuthoringScope
    {
        private MyDeclarations declarationsList;

        public void AddDeclaration(MyDeclaration declaration)
        {
            if (declaration != null)
            {
                if (declarationsList == null)
                {
                    declarationsList = new MyDeclarations();
                }
                declarationsList.AddDeclaration(declaration);
            }
        }

        public override Declarations GetDeclarations(IVsTextView view,
                                                     int line,
                                                     int col,
                                                     TokenInfo info,
                                                     ParseReason reason)
        {
            return declarationsList;
        }

        /////////////////////////////////////////////////
        // Remainder of AuthoringScope methods not shown.
        /////////////////////////////////////////////////
    }


    //===================================================
    class MyLanguageService : LanguageService
    {
        public override AuthoringScope ParseSource(ParseRequest req)
        {
            MyAuthoringScope scope = new MyAuthoringScope();
            if (req.Reason == ParseReason.MemberSelect ||
                req.Reason == ParseReason.MemberSelectAndHighlightBraces)
            {
                // Gather list of declarations based on what the user
                // has typed so far. In this example, this list is an array of
                // MemberDeclaration objects (a helper class you might implement
                // to hold member declarations).
                // How this list is gathered is dependent on the parser
                // and is not shown here.
                MemberDeclarations memberDeclarations;
                memberDeclarations = GetDeclarationsForScope();

                // Now populate the Declarations list in the authoring scope.
                // GetImageIndexBasedOnType() is a helper method you
                // might implement to convert a member type to an index into
                // the image list returned from the language service.
                foreach (MemberDeclaration dec in memberDeclarations)
                {
                    scope.AddDeclaration(new MyDeclaration(
                                             dec.Name,
                                             GetImageIndexBasedOnType(dec.Type),
                                             dec.Description));
                }
            }
            return scope;
        }
    }
}

See Also

Concepts

Language Service Features (Managed Package Framework)