/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; using dnSpy.Contracts.Utilities; using dnSpy.Roslyn.Internal.SignatureHelp; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.Language.Intellisense; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.Text.Editor; namespace dnSpy.Roslyn.Intellisense.SignatureHelp { sealed class SignatureHelpSession { public event EventHandler? Disposed; readonly Lazy signatureHelpBroker; readonly ITextView textView; readonly SignatureHelpService signatureHelpService; readonly List signatures; CancellationTokenSource? cancellationTokenSource; ISignatureHelpSession? session; static readonly object sigHelpSessionKey = new object(); SignatureHelpSession(SignatureHelpService signatureHelpService, Lazy signatureHelpBroker, ITextView textView) { this.signatureHelpBroker = signatureHelpBroker; this.textView = textView; signatures = new List(); this.signatureHelpService = signatureHelpService; } /// /// Gets the Roslyn sig help session stored in a or null if none /// /// Intellisense sig help session /// public static SignatureHelpSession? TryGetSession(ISignatureHelpSession session) { if (session is null) return null; if (session.Properties.TryGetProperty(sigHelpSessionKey, out SignatureHelpSession ourSession)) return ourSession; return null; } public static SignatureHelpSession? TryCreate(SnapshotPoint triggerPosition, SignatureHelpTriggerInfo triggerInfo, Lazy signatureHelpBroker, ITextView textView) { var info = SignatureHelpInfo.Create(triggerPosition.Snapshot); if (info is null) return null; if (triggerInfo.TriggerReason == SignatureHelpTriggerReason.TypeCharCommand) { Debug2.Assert(triggerInfo.TriggerCharacter is not null); if (triggerInfo.TriggerCharacter is not null && !info.Value.SignatureHelpService.IsTriggerCharacter(triggerInfo.TriggerCharacter.Value)) return null; } else if (triggerInfo.TriggerReason == SignatureHelpTriggerReason.RetriggerCommand) { if (triggerInfo.TriggerCharacter is not null && !info.Value.SignatureHelpService.IsRetriggerCharacter(triggerInfo.TriggerCharacter.Value)) return null; } return new SignatureHelpSession(info.Value.SignatureHelpService, signatureHelpBroker, textView); } public void Restart(SnapshotPoint triggerPosition, SignatureHelpTriggerInfo triggerInfo) { if (!RestartCore(triggerPosition, triggerInfo)) Dispose(); } bool RestartCore(SnapshotPoint triggerPosition, SignatureHelpTriggerInfo triggerInfo) { var info = SignatureHelpInfo.Create(triggerPosition.Snapshot); if (info is null) return false; Start(info.Value, triggerPosition, triggerInfo); return true; } void CancelFetchItems() { cancellationTokenSource?.Cancel(); cancellationTokenSource?.Dispose(); cancellationTokenSource = null; } void Start(SignatureHelpInfo info, SnapshotPoint triggerPosition, SignatureHelpTriggerInfo triggerInfo) { CancelFetchItems(); Debug2.Assert(cancellationTokenSource is null); cancellationTokenSource = new CancellationTokenSource(); var cancellationTokenSourceTmp = cancellationTokenSource; StartAsync(info, triggerPosition, triggerInfo, cancellationTokenSource.Token) .ContinueWith(t => { var ex = t.Exception; // Free resources if (cancellationTokenSource == cancellationTokenSourceTmp) CancelFetchItems(); }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext()); } async Task StartAsync(SignatureHelpInfo info, SnapshotPoint triggerPosition, SignatureHelpTriggerInfo triggerInfo, CancellationToken cancellationToken) { // This helps a little to speed up the code ProfileOptimizationHelper.StartProfile("roslyn-sighelp-" + info.SignatureHelpService.Language); var result = await info.SignatureHelpService.GetItemsAsync(info.Document, triggerPosition.Position, triggerInfo, cancellationToken); if (result is null) { Dispose(); return; } if (cancellationToken.IsCancellationRequested) return; StartSession(triggerPosition, result); } ITrackingSpan CreateApplicableToSpan(SnapshotPoint triggerPosition, TextSpan applicableSpan) { var snapshot = triggerPosition.Snapshot; Debug.Assert(applicableSpan.End <= snapshot.Length); Debug.Assert(applicableSpan.Start <= triggerPosition.Position && triggerPosition.Position <= applicableSpan.End); int startPos = Math.Min(applicableSpan.Start, snapshot.Length); int endPos = Math.Max(startPos, Math.Min(Math.Min(triggerPosition.Position, applicableSpan.End), snapshot.Length)); var span = Span.FromBounds(startPos, endPos); return snapshot.CreateTrackingSpan(span, SpanTrackingMode.EdgeInclusive); } void StartSession(SnapshotPoint triggerPosition, SignatureHelpResult signatureHelpResult) { if (isDisposed || textView.IsClosed) { Dispose(); return; } var triggerPoint = triggerPosition.Snapshot.CreateTrackingPoint(triggerPosition.Position, PointTrackingMode.Negative); var applicableSpan = signatureHelpResult.Items.ApplicableSpan; var trackingSpan = CreateApplicableToSpan(triggerPosition, applicableSpan); InitializeSignatures(trackingSpan, signatureHelpResult); if (session is null) { session = signatureHelpBroker.Value.CreateSignatureHelpSession(textView, triggerPoint, trackCaret: false); session.Dismissed += Session_Dismissed; session.Properties.AddProperty(sigHelpSessionKey, this); } session.Recalculate(); // It's set to null if it got dismissed if (session is null || session.IsDismissed) { Debug.Assert(isDisposed); Dispose(); return; } var selectedSig = signatures.FirstOrDefault(a => a.IsSelected); if (selectedSig is not null) session.SelectedSignature = selectedSig; } void Session_Dismissed(object? sender, EventArgs e) => Dispose(); void InitializeSignatures(ITrackingSpan applicableToSpan, SignatureHelpResult signatureHelpResult) { Debug2.Assert(signatureHelpResult.Items is not null); signatures.Clear(); foreach (var item in signatureHelpResult.Items.Items) { bool isSelected = signatureHelpResult.SelectedItem == item; signatures.Add(new Signature(applicableToSpan, item, isSelected, signatureHelpResult.SelectedParameter)); } } public void AugmentSignatureHelpSession(IList signatures) { foreach (var sig in this.signatures) signatures.Add(sig); } public ISignature? GetBestMatch() => session?.SelectedSignature; public bool IsTriggerCharacter(char c) => signatureHelpService.IsTriggerCharacter(c); public bool IsRetriggerCharacter(char c) => signatureHelpService.IsRetriggerCharacter(c); public void Dispose() { if (isDisposed) return; isDisposed = true; CancelFetchItems(); if (session is not null) { session.Dismissed -= Session_Dismissed; session.Dismiss(); } session = null; signatures.Clear(); Disposed?.Invoke(this, EventArgs.Empty); } bool isDisposed; } }