07 Oct
Custom Visual Studio language services: Tracking recently used items in autocompletion lists
The C# language service has the great feature of remembering recently used items in the completion lists (auto-complete, complete word, member select, etc.). You can add a similar ability to your language service by deriving your Declarations-derived class from MruDeclarations instead of Declarations.
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 | public abstract class MruDeclarations : Declarations { static List<string> _recentCompletions = new List<string>(); static void InsertUsedItem( string name ) { if ( string.IsNullOrEmpty( name ) ) return; if ( _recentCompletions.Count >= 20 ) _recentCompletions.RemoveAt( 0 ); _recentCompletions.Remove( name ); _recentCompletions.Add( name ); } public override void GetBestMatch( string value, out int index, out bool uniqueMatch ) { base.GetBestMatch( value, out index, out uniqueMatch ); if ( !uniqueMatch ) { List<string> relevantRecentItems = _recentCompletions.Where( item => item.StartsWith( value, StringComparison.OrdinalIgnoreCase ) ).ToList(); if ( relevantRecentItems.Count == 0 ) return; string last = relevantRecentItems.OrderBy( i => i, StringComparer.OrdinalIgnoreCase ).Last(); var declaration = // start at index to ignore items that fall before the first item that starts with the text typed so-far (case-insensitive) Enumerable.Range( index, GetCount()-1 ) // anonymous type to cache the DisplayText .Select( i => new { DisplayText = GetDisplayText( i ), Index = i } ) // stop taking items when we reach the last relevant recent completion in alphabetical order (case-insensitive) .TakeWhile( item => string.Compare( item.DisplayText, last, StringComparison.OrdinalIgnoreCase ) <= 0 ) // filter on whether the recent completions list contains the item (case-insensitive) .Where( item => relevantRecentItems.Contains( item.DisplayText, StringComparer.OrdinalIgnoreCase ) ) // rank the items that match a recent completion // - give preference to items that were more recently typed // - give preference to case-sensitive matches // => higher rank values indicate better matches .OrderByDescending( item => { int withCase = relevantRecentItems.LastIndexOf( item.DisplayText ); // bias case-insensitive matches to case-sensitive matches have higher rank int withoutCase = relevantRecentItems.FindLastIndex( i => i.Equals( item.DisplayText, StringComparison.OrdinalIgnoreCase ) ) - 1; // Give preference to the more recent match. Note that LastIndexOf() returns -1 if the item wasn't found, // so if the item doesn't have a case-sensitive match, withoutCase always has the higher value. return Math.Max( withCase, withoutCase ); } ) .FirstOrDefault(); if ( declaration != null ) { index = declaration.Index; } } } public override string OnCommit( IVsTextView textView, string textSoFar, char commitCharacter, int index, ref TextSpan initialExtent ) { string committed = base.OnCommit( textView, textSoFar, commitCharacter, index, ref initialExtent ); InsertUsedItem( committed ); return committed; } } |
A very belated thanks for this! Minor fix: the query should begin with
Enumerable.Range( index, GetCount()-index )
instead of
Enumerable.Range( index, GetCount()-1 )
April 4th, 2013 at 2:13 am