ManagedMyC: Type and member dropdown bars
This is part 4 of [many?] posts about creating an ANTLR-based language service for Visual Studio.
Now that we have an AST with information about the top-level members in our source files, we can use the tree parser to gather this information and make it available for the dropdown bars. For now, the MyC language doesn’t support user types like structs, enums, or classes, so we’re stuck with listing the members in the file.
Gathering type & member information
The type & member dropdown bars use instances of the DropDownMember class to present their information. For now, we’ll use this class directly in the gathering stage (in the tree parser). The Visual Studio 2008 SDK version 1.0 has a major bug in the DropDownMember class, and using it will crash Visual Studio. You need to use the version 1.1 SDK instead.
Since the language service (MyCLanguageService) is unaware that the parser uses a separate tree parser to process the AST, add the following properties to the MyCParser class (in your helper file for it). We’ll use them to expose the results from the walker.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | public IList<DropDownMember> Types { get { throw new NotImplementedException(); } } public IList<DropDownMember> Members { get { throw new NotImplementedException(); } } public MyCWalker Walker { get; private set; } |
Since the Types and Members are relevant to a particular source file, we’ll hold these in the MyCSource class, like the braces are. Add the following properties to the MyCSource class:
1 2 3 4 5 6 7 8 9 10 | public IList<DropDownMember> Types { get; set; } public IList<DropDownMember> Members { get; set; } |
The language service moves the types & members to the MyCSource object during the ParseSource request. Find the lines in the MyCLanguageService.ParseSource function where the braces are stored, and store the types & members at the same time:
// store the parse results
source.ParseResult = null;
source.Braces = parser.Braces;
source.Types = parser.Types;
source.Members = parser.Members;
Next, we need to use the tree parser to create a DropDownMember in the Members property for each variable declaration and function definition. To help out, we create the AddType and AddMember functions in MyCWalkerHelper.cs (similar to the Match and Region functions in MyCParserHelper.cs). The helper file becomes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | using System.Collections.Generic; using Antlr.Runtime; using Antlr.Runtime.Tree; using Microsoft.VisualStudio.Package; using Microsoft.VisualStudio.TextManager.Interop; namespace ManagedMyC { partial class MyCWalker { public MyCWalker( MyCParser parser, CommonTree tree ) : this( new CommonTreeNodeStream( tree ) ) { ( (CommonTreeNodeStream)input ).TokenStream = parser.TokenStream; } public List<DropDownMember> UnsortedTypes { get; private set; } public List<DropDownMember> UnsortedMembers { get; private set; } public void WalkAST() { UnsortedTypes = new List<DropDownMember>(); UnsortedMembers = new List<DropDownMember>(); program(); } void AddType( string name, IToken start, IToken stop, IconImageIndex glyph ) { if ( name == null || start == null || stop == null ) return; DropDownMember type = new DropDownMember( name, TextSpanHelper.Merge( MyCParser.ToTextSpan( start ), MyCParser.ToTextSpan( stop ) ), (int)glyph, DROPDOWNFONTATTR.FONTATTR_PLAIN ); UnsortedTypes.Add( type ); } void AddMember( string name, IToken start, IToken stop, IconImageIndex glyph ) { if ( name == null || start == null || stop == null ) return; DropDownMember member = new DropDownMember( name, TextSpanHelper.Merge( MyCParser.ToTextSpan( start ), MyCParser.ToTextSpan( stop ) ), (int)glyph, DROPDOWNFONTATTR.FONTATTR_PLAIN ); UnsortedMembers.Add( member ); } } } |
We update the tree parser to use these functions:
tree grammar MyCWalker; options { language=CSharp2; tokenVocab=MyC; ASTLabelType=CommonTree; } @namespace { ManagedMyC } program : declarations ; declarations : declaration* ; declaration : declaration_ ; declaration_ : ^( AST_FUNCDEF class1? type? IDENTIFIER p=parameters block { AddMember( string.Format( "{0}({1})", $IDENTIFIER.text, $p.ptext ), $IDENTIFIER.Token, $block.close, IconImageIndex.Method ); } ) | simple_declaration ; simple_declarations1 : simple_declaration+ ; simple_declaration : ^( AST_DECL class1? type ( IDENTIFIER { AddMember($IDENTIFIER.text, $IDENTIFIER.Token, $IDENTIFIER.Token, IconImageIndex.Variable); } )+ ) ; parameters returns [string ptext] @init { System.Collections.Generic.Listparam_list = new System.Collections.Generic.List (); } : ^( AST_PARAMS ( parameter { param_list.Add($parameter.text); } )* ) { $ptext = string.Join( ", ", param_list.ToArray() ); } ; parameter : ^(type IDENTIFIER) ; class1 : 'static' | 'auto' | 'extern' ; type : 'int' | 'void' ; block returns [IToken close] : open_block /*block_content1?*/ close_block { $close = $close_block.start.Token; } ; open_block : '{' ; close_block : '}' ;
The final step is to implement the Types & Members properties in MyCParserHelper.cs to return the information from the walker.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | public IList<DropDownMember> Types { get { if ( Walker == null || Walker.UnsortedTypes == null ) return new DropDownMember[0]; return Walker.UnsortedTypes.OrderBy( type => PositionIndicator( type ) ).ToArray(); } } public IList<DropDownMember> Members { get { if ( Walker == null || Walker.UnsortedMembers == null ) return new DropDownMember[0]; return Walker.UnsortedMembers.OrderBy( member => PositionIndicator( member ) ).ToArray(); } } static int PositionIndicator( DropDownMember member ) { return ( member.Span.iStartLine << 10 ) + member.Span.iStartIndex; } |
Using the type & member information for type & member dropdown bars
The TypeAndMemberDropdownBars class is abstract, so we need to derive our own class from it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using Microsoft.VisualStudio.Package; using Microsoft.VisualStudio.TextManager.Interop; using ArrayList = System.Collections.ArrayList; namespace ManagedMyC { public class MyCTypeAndMemberDropdownBars : TypeAndMemberDropdownBars { public MyCTypeAndMemberDropdownBars( MyCLanguageService languageService ) : base( languageService ) { } public override bool OnSynchronizeDropdowns( LanguageService languageService, IVsTextView textView, int line, int col, ArrayList dropDownTypes, ArrayList dropDownMembers, ref int selectedTypeIndex, ref int selectedMemberIndex ) { // TODO return false; } } } |
To use this new class, we overload the CreateDropDownHelper function in MyCLanguageService.
1 2 3 4 | public override TypeAndMemberDropdownBars CreateDropDownHelper( IVsTextView forView ) { return new MyCTypeAndMemberDropdownBars( this ); } |
We’ll also need some helper functions in the MyCSource class to filter the available types and members information. This information may seem like overkill at this point, but as I mentioned earlier, my intention with this post is to demonstrate a “solid implementation of the TypeAndMemberDropdownBars“. This method has proven powerful, flexible, and reliable for several languages so far.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | #region Type & member dropdowns support /// <summary> /// Gets the members of the current Source to show in the Types list of the /// TypeAndMemberDropdownBars. /// </summary> /// <returns>An enumeration of items to show in the Types dropdown.</returns> public virtual IEnumerable<DropDownMember> GetDropDownTypes() { if ( Types == null ) return null; return ( from type in Types orderby type.Label select type ) .ToArray(); } /// <summary> /// Gets the members of the selected type to show in the Members list of the /// TypeAndMemberDropdownBars. /// </summary> /// <param name="type">The item currently selected in the Types dropdown.</param> /// <param name="excludeSpans">A list of TextSpans that are used to filter the list of members. /// Typically, these spans are sub-spans of the selected type representing nested types, where /// members within those spans are actually members of a nested type ant should not be shown.</param> /// <returns>An enumeration of items to show in the Members dropdown.</returns> public virtual IEnumerable<DropDownMember> GetDropDownMembers( DropDownMember type, IEnumerable<TextSpan> excludeSpans ) { if ( Members == null ) return null; var members = ( from member in Members.ToArray() where (type == null) || TextSpanHelper.IsEmbedded( member.Span, type.Span ) where !excludeSpans.Any( span => TextSpanHelper.IsEmbedded( member.Span, span ) ) orderby member.Label select member ) .ToArray(); return members; } /// <summary> /// Gets the default item to show in the Types dropdown of the TypeAndMemberDropdownBars. /// This function is called when the cursor is not in the span of any type in the list. /// </summary> /// <param name="types">The types shown in the Types dropdown.</param> /// <returns>The index of the type that should be visible in the Types dropdown.</returns> public virtual int GetDefaultType( IEnumerable<DropDownMember> types ) { return ( types.FirstOrDefault() == null ) ? -1 : 0; } /// <summary> /// Gets the default item to show in the Members dropdown of the TypeAndMemberDropdownBars. /// This function is called when the cursor is not inside the span of any members of the /// selected type. /// </summary> /// <param name="selectedType">The item currently selected in the Types dropdown.</param> /// <param name="members">The members of the selected type shown in the Members dropdown.</param> /// <returns>The index of the member that should be visible in the Members dropdown.</returns> public virtual int GetDefaultMember( DropDownMember selectedType, IEnumerable<DropDownMember> members ) { return ( members.FirstOrDefault() == null ) ? -1 : 0; } #endregion |
And now for the beast: the implementation of OnSynchronizeDropdowns. Side note: if you find this code helpful, it would make me feel good if you drop me a thanks - there's a lot of work in this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | using System.Collections.Generic; using System.Linq; using Microsoft.VisualStudio.Package; using Microsoft.VisualStudio.TextManager.Interop; using ArrayList = System.Collections.ArrayList; namespace ManagedMyC { public class MyCTypeAndMemberDropdownBars : TypeAndMemberDropdownBars { public MyCTypeAndMemberDropdownBars( MyCLanguageService languageService ) : base( languageService ) { } // If you find this code helpful, it would make me feel good if you drop me a thanks - there's a lot of work in this. public override bool OnSynchronizeDropdowns( LanguageService languageService, IVsTextView textView, int line, int col, ArrayList dropDownTypes, ArrayList dropDownMembers, ref int selectedTypeIndex, ref int selectedMemberIndex ) { MyCSource source = languageService.GetSource( textView ) as MyCSource; if ( source == null ) return false; DropDownMember selectedType = null; DropDownMember selectedMember = null; if ( selectedTypeIndex >= 0 && dropDownTypes.Count > selectedTypeIndex ) selectedType = dropDownTypes[selectedTypeIndex] as DropDownMember; if ( selectedMemberIndex >= 0 && dropDownMembers.Count > selectedMemberIndex ) selectedMember = dropDownMembers[selectedMemberIndex] as DropDownMember; IEnumerable<DropDownMember> types = source.GetDropDownTypes(); if ( types == null ) return false; if ( source.TypesUpdated ) { dropDownTypes.Clear(); dropDownTypes.AddRange( types.ToList() ); source.TypesUpdated = false; } DropDownMember typeScope = ( from type in types where TextSpanHelper.ContainsInclusive( type.Span, line, col ) orderby ( type.Span.iStartLine << 16 ) + type.Span.iStartIndex select type ) .LastOrDefault(); if ( typeScope != null ) selectedTypeIndex = dropDownTypes.IndexOf( typeScope ); else if ( selectedTypeIndex < 0 || selectedTypeIndex >= dropDownTypes.Count ) selectedTypeIndex = source.GetDefaultType( types ); IEnumerable<TextSpan> excludeSpans = null; if ( typeScope != null ) { // exclude sub-spans so members of nested types don't show as members of the current type excludeSpans = from type in types where TextSpanHelper.IsEmbedded( type.Span, typeScope.Span ) where !TextSpanHelper.IsSameSpan( typeScope.Span, type.Span ) select type.Span; } else { excludeSpans = Enumerable.Empty<TextSpan>(); } IEnumerable<DropDownMember> members = source.GetDropDownMembers( typeScope, excludeSpans ); if ( members == null ) return false; if ( source.MembersUpdated || selectedType != typeScope ) { dropDownMembers.Clear(); dropDownMembers.AddRange( members.ToList() ); source.MembersUpdated = false; } DropDownMember memberScope = ( from member in members where TextSpanHelper.ContainsInclusive( member.Span, line, col ) orderby ( member.Span.iStartLine << 16 ) + member.Span.iStartIndex select member ) .LastOrDefault(); if ( memberScope != null ) selectedMemberIndex = dropDownMembers.IndexOf( memberScope ); else if ( selectedMemberIndex < 0 || selectedMemberIndex >= dropDownMembers.Count ) selectedMemberIndex = source.GetDefaultMember( selectedType, members ); if ( selectedTypeIndex >= 0 ) { // gray out the displayed type if the cursor is no longer in its span ( (DropDownMember)dropDownTypes[selectedTypeIndex] ).FontAttr = ( typeScope == null ) ? DROPDOWNFONTATTR.FONTATTR_GRAY : DROPDOWNFONTATTR.FONTATTR_PLAIN; } if ( selectedMemberIndex >= 0 ) { // gray out the displayed member if the cursor is no longer in its span ( (DropDownMember)dropDownMembers[selectedMemberIndex] ).FontAttr = ( memberScope == null ) ? DROPDOWNFONTATTR.FONTATTR_GRAY : DROPDOWNFONTATTR.FONTATTR_PLAIN; } return true; } } } |
Keeping the type & member dropdown bars in sync with the cursor
The default LanguageService implementation calls SynchronizeDropdowns sometimes, but not every time it's needed. To help with the overall feel of the dropdowns, we need to add two more pieces.
First, we need to call SynchronizeDropdowns after a parse is completed. We already have a property UpdateDropdowns inside the MyCLanguageService class that is checked inside OnIdle. We just need to add the following code in ParseSource, inside the switch ( req.Reason ) under case ParseReason.Check:
1 2 | // make sure the dropdowns are updated after a parse UpdateDropdowns = true; |
Second, we need to keep track of when our lists of DropDownMembers in the MyCSource class change:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | IList<DropDownMember> _types; IList<DropDownMember> _members; public bool TypesUpdated { get; set; } public bool MembersUpdated { get; set; } public IList<DropDownMember> Types { get { return _types; } set { _types = value; TypesUpdated = true; } } public IList<DropDownMember> Members { get { return _members; } set { _members = value; MembersUpdated = true; } } |
Source code for this article
Here's the source code for the ManagedMyC sample at this point. Since I surely missed things, you can always diff this code versus the original source from my first post on this subject.
Once again, you’ll have to generate your own LanguageService and Package Guids and set them in MyCConstants.cs before building this project.
The file is compressed with 7-zip because it’s free & awesome.
ManagedMyC-2.7z
Really helpful article. Thanks a lot!
February 3rd, 2009 at 5:59 pmI really want to thank you for the effort that you put in in making Antlr useful for C# from c# developers prespective.Thou i am just a hobbyist and would be using antlr soon your article is the only drop of water i could find for the thirst as to how to do it in c#.
while ( true ) { Many thanks and May God Bless You
System.Windows.Application.DoEvents(); //in case you wanna get a glass of water
}
October 15th, 2009 at 2:08 am