File: SignatureHelp\Presentation\SignatureHelpPresenter.SignatureHelpPresenterSession.cs
Web Access
Project: src\src\EditorFeatures\Core.Wpf\Microsoft.CodeAnalysis.EditorFeatures.Wpf_0tct2hz0_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.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp.Presentation
{
    internal partial class SignatureHelpPresenter
    {
        private sealed class SignatureHelpPresenterSession(
            IThreadingContext threadingContext,
            ISignatureHelpBroker sigHelpBroker,
            ITextView textView) : ISignatureHelpPresenterSession
        {
            private readonly IThreadingContext _threadingContext = threadingContext;
            private readonly ISignatureHelpBroker _sigHelpBroker = sigHelpBroker;
            private readonly ITextView _textView = textView;
 
            public event EventHandler<EventArgs> Dismissed;
            public event EventHandler<SignatureHelpItemEventArgs> ItemSelected;
 
            private IBidirectionalMap<SignatureHelpItem, Signature> _signatureMap;
 
            private IList<SignatureHelpItem> _signatureHelpItems;
            private SignatureHelpItem _selectedItem;
 
            private ISignatureHelpSession _editorSessionOpt;
            private bool _ignoreSelectionStatusChangedEvent;
 
            public bool EditorSessionIsActive => _editorSessionOpt?.IsDismissed == false;
 
            public void PresentItems(
                ITrackingSpan triggerSpan,
                IList<SignatureHelpItem> signatureHelpItems,
                SignatureHelpItem selectedItem,
                int? selectedParameter)
            {
                _signatureHelpItems = signatureHelpItems;
                _selectedItem = selectedItem;
 
                // Create all the editor signatures for the sig help items we have.
                this.CreateSignatures(triggerSpan, selectedParameter);
 
                // It's a new list of items.  Either create the editor session if this is the
                // first time, or ask the editor session that we already have to recalculate.
                if (_editorSessionOpt == null)
                {
                    // We're tracking the caret.  Don't have the editor do it. 
                    _editorSessionOpt = _sigHelpBroker.CreateSignatureHelpSession(
                        _textView,
                        triggerSpan.GetStartTrackingPoint(PointTrackingMode.Negative),
                        trackCaret: false);
 
                    if (_textView is IDebuggerTextView2 debugTextView && !debugTextView.IsImmediateWindow)
                    {
                        debugTextView.HACK_StartCompletionSession(_editorSessionOpt);
                    }
 
                    _editorSessionOpt.Dismissed += (s, e) => OnEditorSessionDismissed();
                    _editorSessionOpt.SelectedSignatureChanged += OnSelectedSignatureChanged;
                }
 
                // So here's the deal.  We cannot create the editor session and give it the right
                // signatures (even though we know what they are).  Instead, the session with
                // call back into the ISignatureHelpSourceProvider (which is us) to get those
                // values. It will pass itself along with the calls back into
                // ISignatureHelpSourceProvider. So, in order to make that connection work, we
                // add properties to the session so that we can call back into ourselves, get
                // the signatures and add it to the session.
                if (!_editorSessionOpt.Properties.ContainsProperty(s_augmentSessionKey))
                {
                    _editorSessionOpt.Properties.AddProperty(s_augmentSessionKey, this);
                }
 
                try
                {
                    // Don't want to get any callbacks while we do this.
                    _ignoreSelectionStatusChangedEvent = true;
 
                    _editorSessionOpt.Recalculate();
 
                    // Now let the editor know what the currently selected item is.
                    Debug.Assert(_signatureMap.ContainsKey(selectedItem));
                    Contract.ThrowIfNull(_signatureMap);
 
                    var defaultValue = _signatureMap.GetValueOrDefault(_selectedItem);
                    if (_editorSessionOpt != null)
                    {
                        _editorSessionOpt.SelectedSignature = defaultValue;
                    }
                }
                finally
                {
                    _ignoreSelectionStatusChangedEvent = false;
                }
            }
 
            private void CreateSignatures(
                ITrackingSpan triggerSpan,
                int? selectedParameter)
            {
                _signatureMap = BidirectionalMap<SignatureHelpItem, Signature>.Empty;
 
                foreach (var item in _signatureHelpItems)
                {
                    _signatureMap = _signatureMap.Add(item, new Signature(triggerSpan, item, GetParameterIndexForItem(item, selectedParameter)));
                }
            }
 
            private static int GetParameterIndexForItem(SignatureHelpItem item, int? selectedParameter)
            {
                if (selectedParameter.HasValue)
                {
                    if (selectedParameter.Value < item.Parameters.Length)
                    {
                        // If the selected parameter is within the range of parameters of this item then set
                        // that as the current parameter.
                        return selectedParameter.Value;
                    }
                    else if (item.IsVariadic)
                    {
                        // It wasn't in range, but the item takes an unlimited number of parameters.  So
                        // just set current parameter to the last parameter (the variadic one).
                        return item.Parameters.Length - 1;
                    }
                }
 
                // It was out of bounds, there is no current parameter now.
                return -1;
            }
 
            private void OnEditorSessionDismissed()
            {
                _threadingContext.ThrowIfNotOnUIThread();
                this.Dismissed?.Invoke(this, new EventArgs());
            }
 
            private void OnSelectedSignatureChanged(object sender, SelectedSignatureChangedEventArgs eventArgs)
            {
                _threadingContext.ThrowIfNotOnUIThread();
 
                if (_ignoreSelectionStatusChangedEvent)
                {
                    return;
                }
 
                Contract.ThrowIfFalse(_signatureMap.TryGetKey((Signature)eventArgs.NewSelectedSignature, out var helpItem));
 
                var helpItemSelected = this.ItemSelected;
                if (helpItemSelected != null && helpItem != null)
                {
                    helpItemSelected(this, new SignatureHelpItemEventArgs(helpItem));
                }
            }
 
            public void Dismiss()
            {
                _threadingContext.ThrowIfNotOnUIThread();
 
                if (_editorSessionOpt == null)
                {
                    // No editor session, nothing to do here.
                    return;
                }
 
                _editorSessionOpt.Dismiss();
                _editorSessionOpt = null;
            }
 
            private bool ExecuteKeyboardCommand(IntellisenseKeyboardCommand command)
            {
                var target = _editorSessionOpt != null
                    ? _editorSessionOpt.Presenter as IIntellisenseCommandTarget
                    : null;
 
                return target != null && target.ExecuteKeyboardCommand(command);
            }
 
            public void SelectPreviousItem()
                => ExecuteKeyboardCommand(IntellisenseKeyboardCommand.Up);
 
            public void SelectNextItem()
                => ExecuteKeyboardCommand(IntellisenseKeyboardCommand.Down);
 
            // Call backs from our ISignatureHelpSourceProvider.  Used to actually populate the vs
            // session.
            internal void AugmentSignatureHelpSession(IList<ISignature> signatures)
            {
                signatures.Clear();
                signatures.AddRange(_signatureHelpItems.Select(_signatureMap.GetValueOrDefault));
            }
        }
    }
}