|
// 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 System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.SignatureHelp;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.SignatureHelp
{
internal partial class Controller
{
internal partial class Session
{
public void ComputeModel(
ImmutableArray<ISignatureHelpProvider> providers,
SignatureHelpTriggerInfo triggerInfo)
{
this.Computation.ThreadingContext.ThrowIfNotOnUIThread();
var caretPosition = Controller.TextView.GetCaretPoint(Controller.SubjectBuffer).Value;
var disconnectedBufferGraph = new DisconnectedBufferGraph(Controller.SubjectBuffer, Controller.TextView.TextBuffer);
// If we've already computed a model, then just use that. Otherwise, actually
// compute a new model and send that along.
Computation.ChainTaskAndNotifyControllerWhenFinished(
(model, cancellationToken) => ComputeModelInBackgroundAsync(
model, providers, caretPosition, disconnectedBufferGraph,
triggerInfo, cancellationToken));
}
private async Task<Model> ComputeModelInBackgroundAsync(
Model currentModel,
ImmutableArray<ISignatureHelpProvider> providers,
SnapshotPoint caretPosition,
DisconnectedBufferGraph disconnectedBufferGraph,
SignatureHelpTriggerInfo triggerInfo,
CancellationToken cancellationToken)
{
try
{
using (Logger.LogBlock(FunctionId.SignatureHelp_ModelComputation_ComputeModelInBackground, cancellationToken))
{
this.Computation.ThreadingContext.ThrowIfNotOnBackgroundThread();
cancellationToken.ThrowIfCancellationRequested();
var document = Controller.DocumentProvider.GetDocument(caretPosition.Snapshot, cancellationToken);
if (document == null)
{
return currentModel;
}
// Let LSP handle signature help in the cloud scenario
if (Controller.SubjectBuffer.IsInLspEditorContext())
{
return null;
}
if (triggerInfo.TriggerReason == SignatureHelpTriggerReason.RetriggerCommand)
{
if (currentModel == null)
{
return null;
}
if (triggerInfo.TriggerCharacter.HasValue &&
!currentModel.Provider.IsRetriggerCharacter(triggerInfo.TriggerCharacter.Value))
{
return currentModel;
}
}
var options = Controller.GlobalOptions.GetSignatureHelpOptions(document.Project.Language);
// first try to query the providers that can trigger on the specified character
var (provider, items) = await ComputeItemsAsync(
providers, caretPosition, triggerInfo,
options, document, cancellationToken).ConfigureAwait(false);
if (provider == null)
{
// No provider produced items. So we can't produce a model
return null;
}
if (currentModel != null &&
currentModel.Provider == provider &&
currentModel.GetCurrentSpanInSubjectBuffer(disconnectedBufferGraph.SubjectBufferSnapshot).Span.Start == items.ApplicableSpan.Start &&
currentModel.Items.IndexOf(currentModel.SelectedItem) == items.SelectedItemIndex &&
currentModel.ArgumentIndex == items.ArgumentIndex &&
currentModel.ArgumentCount == items.ArgumentCount &&
currentModel.ArgumentName == items.ArgumentName)
{
// The new model is the same as the current model. Return the currentModel
// so we keep the active selection.
return currentModel;
}
var selectedItem = GetSelectedItem(currentModel, items, provider, out var userSelected);
var model = new Model(disconnectedBufferGraph, items.ApplicableSpan, provider,
items.Items, selectedItem, items.ArgumentIndex, items.ArgumentCount, items.ArgumentName,
selectedParameter: 0, userSelected);
var syntaxFactsService = document.GetLanguageService<ISyntaxFactsService>();
var isCaseSensitive = syntaxFactsService == null || syntaxFactsService.IsCaseSensitive;
var selection = DefaultSignatureHelpSelector.GetSelection(model.Items,
model.SelectedItem, model.UserSelected, model.ArgumentIndex, model.ArgumentCount, model.ArgumentName, isCaseSensitive);
return model.WithSelectedItem(selection.SelectedItem, selection.UserSelected)
.WithSelectedParameter(selection.SelectedParameter);
}
}
catch (Exception e) when (FatalError.ReportAndPropagateUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
{
throw ExceptionUtilities.Unreachable();
}
}
private static SignatureHelpItem GetSelectedItem(Model currentModel, SignatureHelpItems items, ISignatureHelpProvider provider, out bool userSelected)
{
// Try to find the most appropriate item in the list to select by default.
// If it's the same provider as the previous model we have, and we had a user-selection,
// then try to return the user-selection.
if (currentModel != null && currentModel.Provider == provider && currentModel.UserSelected)
{
var userSelectedItem = items.Items.FirstOrDefault(i => DisplayPartsMatch(i, currentModel.SelectedItem));
if (userSelectedItem != null)
{
userSelected = true;
return userSelectedItem;
}
}
userSelected = false;
// If the provider specified a selected item, then pick that one.
if (items.SelectedItemIndex.HasValue)
{
return items.Items[items.SelectedItemIndex.Value];
}
SignatureHelpItem lastSelectionOrDefault = null;
if (currentModel != null && currentModel.Provider == provider)
{
// If the provider did not pick a default, and it's the same provider as the previous
// model we have, then try to return the same item that we had before.
lastSelectionOrDefault = items.Items.FirstOrDefault(i => DisplayPartsMatch(i, currentModel.SelectedItem));
}
// Otherwise, just pick the first item we have.
lastSelectionOrDefault ??= items.Items.First();
return lastSelectionOrDefault;
}
private static bool DisplayPartsMatch(SignatureHelpItem i1, SignatureHelpItem i2)
=> i1.GetAllParts().SequenceEqual(i2.GetAllParts(), CompareParts);
private static bool CompareParts(TaggedText p1, TaggedText p2)
=> p1.ToString() == p2.ToString();
private static async Task<(ISignatureHelpProvider provider, SignatureHelpItems items)> ComputeItemsAsync(
ImmutableArray<ISignatureHelpProvider> providers,
SnapshotPoint caretPosition,
SignatureHelpTriggerInfo triggerInfo,
SignatureHelpOptions options,
Document document,
CancellationToken cancellationToken)
{
try
{
ISignatureHelpProvider bestProvider = null;
SignatureHelpItems bestItems = null;
// TODO(cyrusn): We're calling into extensions, we need to make ourselves resilient
// to the extension crashing.
foreach (var provider in providers)
{
cancellationToken.ThrowIfCancellationRequested();
var currentItems = await provider.GetItemsAsync(document, caretPosition, triggerInfo, options, cancellationToken).ConfigureAwait(false);
if (currentItems != null && currentItems.ApplicableSpan.IntersectsWith(caretPosition.Position))
{
// If another provider provides sig help items, then only take them if they
// start after the last batch of items. i.e. we want the set of items that
// conceptually are closer to where the caret position is. This way if you have:
//
// Goo(new Bar($$
//
// Then invoking sig help will only show the items for "new Bar(" and not also
// the items for "Goo(..."
if (IsBetter(bestItems, currentItems.ApplicableSpan))
{
bestItems = currentItems;
bestProvider = provider;
}
}
}
return (bestProvider, bestItems);
}
catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken, ErrorSeverity.Critical))
{
return (null, null);
}
}
private static bool IsBetter(SignatureHelpItems bestItems, TextSpan? currentTextSpan)
{
// If we have no best text span, then this span is definitely better.
if (bestItems == null)
{
return true;
}
// Otherwise we want the one that is conceptually the innermost signature. So it's
// only better if the distance from it to the caret position is less than the best
// one so far.
return currentTextSpan.Value.Start > bestItems.ApplicableSpan.Start;
}
}
}
}
|