Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On-the-fly-docs -- Add quota exceeded dialogue (#76877) #76893

Open
wants to merge 1 commit into
base: release/dev17.13
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 93 additions & 10 deletions src/EditorFeatures/Core.Wpf/QuickInfo/OnTheFlyDocsView.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
Expand All @@ -20,6 +23,9 @@
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.PlatformUI;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Telemetry;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;
Expand All @@ -40,6 +46,8 @@ internal sealed partial class OnTheFlyDocsView : UserControl, INotifyPropertyCha
private readonly OnTheFlyDocsInfo _onTheFlyDocsInfo;
private readonly ContentControl _responseControl = new();
private readonly CancellationTokenSource _cancellationTokenSource = new();
private readonly List<ClassifiedTextRun> quotaExceededContent;
private readonly IServiceProvider _serviceProvider;

private OnTheFlyDocsState _currentState = OnTheFlyDocsState.OnDemandLink;

Expand All @@ -51,14 +59,21 @@ internal sealed partial class OnTheFlyDocsView : UserControl, INotifyPropertyCha
/// </summary>
public event EventHandler ResultsRequested;

/// <summary>
/// Event that fires when the user requests to upgrade their Copilot plan.
/// </summary>
public event EventHandler? PlanUpgradeRequested;

#pragma warning disable CA1822 // Mark members as static
/// <summary>
/// Used to display the "On the fly documentation" directly in the associated XAML file.
/// </summary>
public string OnTheFlyDocumentation => EditorFeaturesResources.On_the_fly_documentation;
#pragma warning restore CA1822 // Mark members as static

public OnTheFlyDocsView(ITextView textView, IViewElementFactoryService viewElementFactoryService, IAsynchronousOperationListenerProvider listenerProvider, IAsyncQuickInfoSession asyncQuickInfoSession, IThreadingContext threadingContext, QuickInfoOnTheFlyDocsElement onTheFlyDocsElement)
public OnTheFlyDocsView(ITextView textView, IViewElementFactoryService viewElementFactoryService,
IAsynchronousOperationListenerProvider listenerProvider, IAsyncQuickInfoSession asyncQuickInfoSession,
IThreadingContext threadingContext, QuickInfoOnTheFlyDocsElement onTheFlyDocsElement, IServiceProvider serviceProvider)
{
_textView = textView;
_viewElementFactoryService = viewElementFactoryService;
Expand All @@ -67,6 +82,7 @@ public OnTheFlyDocsView(ITextView textView, IViewElementFactoryService viewEleme
_threadingContext = threadingContext;
_onTheFlyDocsInfo = onTheFlyDocsElement.Info;
_document = onTheFlyDocsElement.Document;
_serviceProvider = serviceProvider;

var sparkle = new ImageElement(new VisualStudio.Core.Imaging.ImageId(CopilotConstants.CopilotIconMonikerGuid, CopilotConstants.CopilotIconSparkleId));
object onDemandLinkText = _onTheFlyDocsInfo.IsContentExcluded
Expand Down Expand Up @@ -111,6 +127,34 @@ public OnTheFlyDocsView(ITextView textView, IViewElementFactoryService viewEleme
_responseControl,
]));

// Locates the "upgrade now" link in the localized text, surrounded by square brackets.
var quotaExceededMatch = Regex.Match(
EditorFeaturesResources.Chat_limit_reached_upgrade_now_or_wait_for_the_limit_to_reset,
@"^(.*)\[(.*)\](.*)$");
if (quotaExceededMatch == null)
{
// The text wasn't localized correctly. Assert and fallback to showing it verbatim.
Debug.Fail("Copilot Hover quota exceeded message was not correctly localized.");
quotaExceededContent = [new ClassifiedTextRun(
ClassifiedTextElement.TextClassificationTypeName,
EditorFeaturesResources.Chat_limit_reached_upgrade_now_or_wait_for_the_limit_to_reset)];
}
else
{
quotaExceededContent = [
new ClassifiedTextRun(
ClassifiedTextElement.TextClassificationTypeName,
quotaExceededMatch.Groups[1].Value),
new ClassifiedTextRun(
ClassifiedTextElement.TextClassificationTypeName,
quotaExceededMatch.Groups[2].Value,
() => this.PlanUpgradeRequested?.Invoke(this, EventArgs.Empty)),
new ClassifiedTextRun(
ClassifiedTextElement.TextClassificationTypeName,
quotaExceededMatch.Groups[3].Value),
];
}

ResultsRequested += (_, _) => PopulateAIDocumentationElements(_cancellationTokenSource.Token);
_asyncQuickInfoSession.StateChanged += (_, _) => OnQuickInfoSessionChanged();
InitializeComponent();
Expand All @@ -135,31 +179,61 @@ private async Task SetResultTextAsync(ICopilotCodeAnalysisService copilotService

try
{
var response = await copilotService.GetOnTheFlyDocsAsync(_onTheFlyDocsInfo.SymbolSignature, _onTheFlyDocsInfo.DeclarationCode, _onTheFlyDocsInfo.Language, cancellationToken).ConfigureAwait(false);
var (responseString, isQuotaExceeded) = await copilotService.GetOnTheFlyDocsAsync(_onTheFlyDocsInfo.SymbolSignature, _onTheFlyDocsInfo.DeclarationCode, _onTheFlyDocsInfo.Language, cancellationToken).ConfigureAwait(false);
var copilotRequestTime = stopwatch.Elapsed;

await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);

cancellationToken.ThrowIfCancellationRequested();

if (response is null || response.Length == 0)
if (string.IsNullOrEmpty(responseString))
{
SetResultText(EditorFeaturesResources.An_error_occurred_while_generating_documentation_for_this_code);
CurrentState = OnTheFlyDocsState.Finished;
Logger.Log(FunctionId.Copilot_On_The_Fly_Docs_Error_Displayed, KeyValueLogMessage.Create(m =>
// If the responseStatus is 8, then that means the quota has been exceeded.
if (isQuotaExceeded)
{
m["ElapsedTime"] = copilotRequestTime;
}, LogLevel.Information));
this.PlanUpgradeRequested += (_, _) =>
{
// GUID and command ID from
// https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/45121/Free-SKU-Handling-Guidance-and-Recommendations
var uiShell = _serviceProvider.GetServiceOnMainThread<SVsUIShell, IVsUIShell>();
uiShell.PostExecCommand(
new Guid("39B0DEDE-D931-4A92-9AA2-3447BC4998DC"),
0x3901,
nCmdexecopt: 0,
pvaIn: null);

_asyncQuickInfoSession.DismissAsync();

// Telemetry to track when users reach the quota of the Copilot Free plan.
var telemetryEvent = new OperationEvent(
"vs/copilot/showcopilotfreestatus",
TelemetryResult.Success);
telemetryEvent.Properties["vs.copilot.source"] = "CSharpOnTheFlyDocs";
TelemetryService.DefaultSession.PostEvent(telemetryEvent);
};

ShowQuotaExceededResult();
}
else
{
SetResultText(EditorFeaturesResources.An_error_occurred_while_generating_documentation_for_this_code);
Logger.Log(FunctionId.Copilot_On_The_Fly_Docs_Error_Displayed, KeyValueLogMessage.Create(m =>
{
m["ElapsedTime"] = copilotRequestTime;
}, LogLevel.Information));
}

CurrentState = OnTheFlyDocsState.Finished;
}
else
{
SetResultText(response);
SetResultText(responseString);
CurrentState = OnTheFlyDocsState.Finished;

Logger.Log(FunctionId.Copilot_On_The_Fly_Docs_Results_Displayed, KeyValueLogMessage.Create(m =>
{
m["ElapsedTime"] = copilotRequestTime;
m["ResponseLength"] = response.Length;
m["ResponseLength"] = responseString.Length;
}, LogLevel.Information));
}
}
Expand Down Expand Up @@ -222,6 +296,15 @@ public void SetResultText(string text)
new ContainerElement(ContainerElementStyle.Wrapped, new ClassifiedTextElement([new ClassifiedTextRun(ClassificationTypeNames.Text, text)])));
}

/// <summary>
/// Shows a result message for exceeding the quota of the Copilot Free plan.
/// </summary>
public void ShowQuotaExceededResult()
{
_responseControl.Content = ToUIElement(new ContainerElement(ContainerElementStyle.Stacked,
[new ContainerElement(ContainerElementStyle.Wrapped, new ClassifiedTextElement(this.quotaExceededContent))]));
}

private void OnPropertyChanged<T>(ref T member, T value, [CallerMemberName] string? name = null)
{
member = value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.CodeAnalysis.QuickInfo.Presentation;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.Language.Intellisense;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Text.Adornments;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
Expand All @@ -28,15 +29,18 @@ internal sealed class OnTheFlyDocsViewFactory : IViewElementFactory
private readonly IAsynchronousOperationListenerProvider _listenerProvider;
private readonly IAsyncQuickInfoBroker _asyncQuickInfoBroker;
private readonly IThreadingContext _threadingContext;
private readonly IServiceProvider _serviceProvider;

[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public OnTheFlyDocsViewFactory(IViewElementFactoryService factoryService, IAsynchronousOperationListenerProvider listenerProvider, IAsyncQuickInfoBroker asyncQuickInfoBroker, IThreadingContext threadingContext)
public OnTheFlyDocsViewFactory(IViewElementFactoryService factoryService, IAsynchronousOperationListenerProvider listenerProvider,
IAsyncQuickInfoBroker asyncQuickInfoBroker, IThreadingContext threadingContext, SVsServiceProvider serviceProvider)
{
_factoryService = factoryService;
_listenerProvider = listenerProvider;
_asyncQuickInfoBroker = asyncQuickInfoBroker;
_threadingContext = threadingContext;
_serviceProvider = serviceProvider;
}

public TView? CreateViewElement<TView>(ITextView textView, object model) where TView : class
Expand Down Expand Up @@ -64,6 +68,6 @@ public OnTheFlyDocsViewFactory(IViewElementFactoryService factoryService, IAsync
OnTheFlyDocsLogger.LogShowedOnTheFlyDocsLinkWithDocComments();
}

return new OnTheFlyDocsView(textView, _factoryService, _listenerProvider, quickInfoSession, _threadingContext, onTheFlyDocsElement) as TView;
return new OnTheFlyDocsView(textView, _factoryService, _listenerProvider, quickInfoSession, _threadingContext, onTheFlyDocsElement, _serviceProvider) as TView;
}
}
4 changes: 4 additions & 0 deletions src/EditorFeatures/Core/EditorFeaturesResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -950,4 +950,8 @@ Do you want to proceed?</value>
<data name="Describe_with_Copilot_is_unavailable_since_the_referenced_document_is_excluded_by_your_organization" xml:space="preserve">
<value>'Describe with Copilot' is unavailable since the referenced document is excluded by your organization.</value>
</data>
<data name="Chat_limit_reached_upgrade_now_or_wait_for_the_limit_to_reset" xml:space="preserve">
<value>Chat limit reached, [upgrade now] or wait for the limit to reset.</value>
<comment>The text surrounded by "[" and "]" characters will be hyperlinked. Please ensure the localized text still has "[" and "]" characters.</comment>
</data>
</root>
5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.es.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.fr.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.it.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.ja.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.ko.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/EditorFeatures/Core/xlf/EditorFeaturesResources.pl.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading