Sam's Blog

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;
    }
}

One Response to “Custom Visual Studio language services: Tracking recently used items in autocompletion lists”

  1. 1
    Alex Says:

    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 )

Leave a Reply

© 2014 Sam's Blog | Entries (RSS) and Comments (RSS)

Your Index Web Directorywordpress logo