File: SignatureHelp\Controller_TypeChar.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_a0rtafw3_wpftmp.csproj (Microsoft.CodeAnalysis.EditorFeatures.Wpf)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.VisualStudio.Commanding;
using Microsoft.VisualStudio.Text.Editor.Commanding.Commands;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp;
 
internal sealed partial class Controller
{
    CommandState IChainedCommandHandler<TypeCharCommandArgs>.GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextHandler)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        // We just defer to the editor here.  We do not interfere with typing normal characters.
        return nextHandler();
    }
 
    void IChainedCommandHandler<TypeCharCommandArgs>.ExecuteCommand(TypeCharCommandArgs args, Action nextHandler, CommandExecutionContext context)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        var allProviders = GetProviders();
        if (allProviders == null)
        {
            nextHandler();
            return;
        }
 
        // Note: while we're doing this, we don't want to hear about buffer changes (since we
        // know they're going to happen).  So we disconnect and reconnect to the event
        // afterwards.  That way we can hear about changes to the buffer that don't happen
        // through us.
        this.TextView.TextBuffer.PostChanged -= OnTextViewBufferPostChanged;
        try
        {
            nextHandler();
        }
        finally
        {
            this.TextView.TextBuffer.PostChanged += OnTextViewBufferPostChanged;
        }
 
        // We only want to process typechar if it is a normal typechar and no one else is
        // involved.  i.e. if there was a typechar, but someone processed it and moved the caret
        // somewhere else then we don't want signature help.  Also, if a character was typed but
        // something intercepted and placed different text into the editor, then we don't want
        // to proceed. 
        //
        // Note: we do not want to pass along a text version here.  It is expected that multiple
        // version changes may happen when we call 'nextHandler' and we will still want to
        // proceed.  For example, if the user types "WriteL(", then that will involve two text
        // changes as completion commits that out to "WriteLine(".  But we still want to provide
        // sig help in this case.
        if (this.TextView.TypeCharWasHandledStrangely(this.SubjectBuffer, args.TypedChar))
        {
            // If we were computing anything, we stop.  We only want to process a typechar
            // if it was a normal character.
            DismissSessionIfActive();
            return;
        }
 
        // Separate the sig help providers into two buckets; one bucket for those that were triggered
        // by the typed character, and those that weren't.  To keep our queries to a minimum, we first
        // check with the textually triggered providers.  If none of those produced any sig help items
        // then we query the other providers to see if they can produce anything viable.  This takes
        // care of cases where the filtered set of providers didn't provide anything but one of the
        // other providers could still be valid, but doesn't explicitly treat the typed character as
        // a trigger character.
        var (textuallyTriggeredProviders, untriggeredProviders) = FilterProviders(allProviders, args.TypedChar);
        var triggerInfo = new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.TypeCharCommand, args.TypedChar);
 
        if (!IsSessionActive)
        {
            // No computation at all.  If this is not a trigger character, we just ignore it and
            // stay in this state.  Otherwise, if it's a trigger character, start up a new
            // computation and start computing the model in the background.
            if (textuallyTriggeredProviders.Any())
            {
                // First create the session that represents that we now have a potential 
                // signature help list. Then tell it to start computing.
                StartSession(textuallyTriggeredProviders, triggerInfo);
                return;
            }
            else
            {
                // No need to do anything.  Just stay in the state where we have no session.
                return;
            }
        }
        else
        {
            var computed = false;
            if (allProviders.Any(static (p, args) => p.RetriggerCharacters.Contains(args.TypedChar), args))
            {
                // The user typed a character that might close the scope of the current model.
                // In this case, we should requery all providers.
                //
                // e.g.     Math.Max(Math.Min(1,2)$$
                sessionOpt.ComputeModel(allProviders, new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.RetriggerCommand, triggerInfo.TriggerCharacter));
                computed = true;
            }
 
            if (textuallyTriggeredProviders.Any())
            {
                // The character typed was something like "(".  It can both filter a list if
                // it was in a string like: Goo(bar, "(
                //
                // Or it can trigger a new list. Ask the computation to compute again.
                sessionOpt.ComputeModel(
                    textuallyTriggeredProviders.Concat(untriggeredProviders), triggerInfo);
                computed = true;
            }
 
            if (!computed)
            {
                // A character was typed and we haven't updated our model; do so now.
                sessionOpt.ComputeModel(allProviders, new SignatureHelpTriggerInfo(SignatureHelpTriggerReason.RetriggerCommand));
            }
        }
    }
 
    private (ImmutableArray<ISignatureHelpProvider> matched, ImmutableArray<ISignatureHelpProvider> unmatched) FilterProviders(
        ImmutableArray<ISignatureHelpProvider> providers, char ch)
    {
        this.ThreadingContext.ThrowIfNotOnUIThread();
 
        using var matchedProvidersDisposer = ArrayBuilder<ISignatureHelpProvider>.GetInstance(out var matchedProviders);
        using var unmatchedProvidersDisposer = ArrayBuilder<ISignatureHelpProvider>.GetInstance(out var unmatchedProviders);
        foreach (var provider in providers)
        {
            if (provider.TriggerCharacters.Contains(ch))
            {
                matchedProviders.Add(provider);
            }
            else
            {
                unmatchedProviders.Add(provider);
            }
        }
 
        return (matchedProviders.ToImmutable(), unmatchedProviders.ToImmutable());
    }
}