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

Cherrypick (#73873) into 17.11 #74503

Merged
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,19 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
using Microsoft.CodeAnalysis.GoToDefinition;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Options;

namespace Microsoft.CodeAnalysis.Editor.CSharp.InlineRename;
Expand All @@ -18,4 +28,135 @@ internal sealed class CSharpEditorInlineRenameService(
[ImportMany] IEnumerable<IRefactorNotifyService> refactorNotifyServices,
IGlobalOptionService globalOptions) : AbstractEditorInlineRenameService(refactorNotifyServices, globalOptions)
{
private const int NumberOfContextLines = 20;
private const int MaxDefinitionCount = 10;
private const int MaxReferenceCount = 50;

/// <summary>
/// Uses semantic information of renamed symbol to produce a map containing contextual information for use in Copilot rename feature
/// </summary>
/// <returns>Map where key indicates the kind of semantic information, and value is an array of relevant code snippets.</returns>
public override async Task<ImmutableDictionary<string, ImmutableArray<string>>> GetRenameContextAsync(
IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken)
{
using var _1 = PooledHashSet<TextSpan>.GetInstance(out var seen);
using var _2 = ArrayBuilder<string>.GetInstance(out var definitions);
using var _3 = ArrayBuilder<string>.GetInstance(out var references);
using var _4 = ArrayBuilder<string>.GetInstance(out var docComments);

foreach (var renameDefinition in inlineRenameInfo.DefinitionLocations.Take(MaxDefinitionCount))
{
// Find largest snippet of code that represents the definition
var containingStatementOrDeclarationSpan =
await TryGetSurroundingNodeSpanAsync<MemberDeclarationSyntax>(renameDefinition.Document, renameDefinition.SourceSpan, cancellationToken).ConfigureAwait(false) ??
await TryGetSurroundingNodeSpanAsync<StatementSyntax>(renameDefinition.Document, renameDefinition.SourceSpan, cancellationToken).ConfigureAwait(false);

// Find documentation comments of definitions
var symbolService = renameDefinition.Document.GetRequiredLanguageService<IGoToDefinitionSymbolService>();
if (symbolService is not null)
{
var textSpan = inlineRenameInfo.TriggerSpan;
var (symbol, _, _) = await symbolService.GetSymbolProjectAndBoundSpanAsync(
renameDefinition.Document, textSpan.Start, cancellationToken)
.ConfigureAwait(true);
var docComment = symbol?.GetDocumentationCommentXml(expandIncludes: true, cancellationToken: cancellationToken);
if (!string.IsNullOrWhiteSpace(docComment))
{
docComments.Add(docComment!);
}
}

var documentText = await renameDefinition.Document.GetTextAsync(cancellationToken).ConfigureAwait(false);
AddSpanOfInterest(documentText, renameDefinition.SourceSpan, containingStatementOrDeclarationSpan, definitions);
}

foreach (var renameLocation in inlineRenameLocationSet.Locations.Take(MaxReferenceCount))
{
// Find largest snippet of code that represents the reference
var containingStatementOrDeclarationSpan =
await TryGetSurroundingNodeSpanAsync<MemberDeclarationSyntax>(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false) ??
await TryGetSurroundingNodeSpanAsync<BaseMethodDeclarationSyntax>(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false) ??
await TryGetSurroundingNodeSpanAsync<StatementSyntax>(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false);

var documentText = await renameLocation.Document.GetTextAsync(cancellationToken).ConfigureAwait(false);
AddSpanOfInterest(documentText, renameLocation.TextSpan, containingStatementOrDeclarationSpan, references);
}

var contextBuilder = ImmutableDictionary.CreateBuilder<string, ImmutableArray<string>>();
if (!definitions.IsEmpty)
{
contextBuilder.Add("definition", definitions.ToImmutable());
}
if (!references.IsEmpty)
{
contextBuilder.Add("reference", references.ToImmutable());
}
if (!docComments.IsEmpty)
{
contextBuilder.Add("documentation", docComments.ToImmutable());
}

return contextBuilder.ToImmutableDictionary();

void AddSpanOfInterest(SourceText documentText, TextSpan fallbackSpan, TextSpan? surroundingSpanOfInterest, ArrayBuilder<string> resultBuilder)
{
int startPosition, endPosition, startLine = 0, endLine = 0, lineCount = 0;
if (surroundingSpanOfInterest is not null)
{
startPosition = surroundingSpanOfInterest.Value.Start;
endPosition = surroundingSpanOfInterest.Value.End;
startLine = documentText.Lines.GetLineFromPosition(surroundingSpanOfInterest.Value.Start).LineNumber;
endLine = documentText.Lines.GetLineFromPosition(surroundingSpanOfInterest.Value.End).LineNumber;
lineCount = endLine - startLine + 1;

if (lineCount > NumberOfContextLines * 2)
{
// The computed span is too large, trim it such that the fallback span is included
// and no content is provided from before startLine or after endLine.
var fallbackStartLine = Math.Max(0, documentText.Lines.GetLineFromPosition(fallbackSpan.Start).LineNumber - NumberOfContextLines);
var fallbackEndLine = Math.Min(documentText.Lines.Count - 1, documentText.Lines.GetLineFromPosition(fallbackSpan.End).LineNumber + NumberOfContextLines);
var excessAtStart = startLine - fallbackStartLine;
var excessAtEnd = fallbackEndLine - endLine;
if (excessAtStart > 0)
{
// The fallback span extends before the relevant span (startLine)
endLine = Math.Min(documentText.Lines.Count - 1, fallbackEndLine + excessAtStart);
}
else if (excessAtEnd > 0)
{
// The fallback span extends after the relevant span (endLine)
startLine = Math.Max(0, fallbackStartLine - excessAtEnd);
}
else
{
// Fallback span surrounds the renamed identifier completely within startLine-endLine span. Use the fallback span as is.
startLine = fallbackStartLine;
endLine = fallbackEndLine;
}
lineCount = endLine - startLine + 1;
}
}

// If a well defined surrounding span was not computed,
// select a span that encompasses NumberOfContextLines lines above and NumberOfContextLines lines below the identifier.
if (surroundingSpanOfInterest is null || lineCount <= 0)
{
startLine = Math.Max(0, documentText.Lines.GetLineFromPosition(fallbackSpan.Start).LineNumber - NumberOfContextLines);
endLine = Math.Min(documentText.Lines.Count - 1, documentText.Lines.GetLineFromPosition(fallbackSpan.End).LineNumber + NumberOfContextLines);
}

// If the start and end positions are not at the beginning and end of the start and end lines respectively,
// expand to select the corresponding lines completely.
startPosition = documentText.Lines[startLine].Start;
endPosition = documentText.Lines[endLine].End;
var length = endPosition - startPosition + 1;

surroundingSpanOfInterest = new TextSpan(startPosition, length);

if (seen.Add(surroundingSpanOfInterest.Value))
{
resultBuilder.Add(documentText.GetSubText(surroundingSpanOfInterest.Value).ToString());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,23 @@
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.CodeAnalysis.Editor;
using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename;
using Microsoft.CodeAnalysis.Editor.InlineRename;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.EditorFeatures.Lightup;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.VisualStudio.PlatformUI;

namespace Microsoft.CodeAnalysis.InlineRename.UI.SmartRename;

Expand All @@ -29,7 +31,7 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi

private readonly IGlobalOptionService _globalOptionService;
private readonly IThreadingContext _threadingContext;
private readonly IAsynchronousOperationListenerProvider _listenerProvider;
private readonly IAsynchronousOperationListener _asyncListener;
private CancellationTokenSource? _cancellationTokenSource;
private bool _isDisposed;
private TimeSpan AutomaticFetchDelay => _smartRenameSession.AutomaticFetchDelay;
Expand Down Expand Up @@ -65,6 +67,11 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi
/// </summary>
public bool IsAutomaticSuggestionsEnabled { get; private set; }

/// <summary>
/// Determines whether smart rename gets semantic context to augment the request for suggested names.
/// </summary>
public bool IsUsingSemanticContext { get; }

private string? _selectedSuggestedName;

/// <summary>
Expand Down Expand Up @@ -109,17 +116,18 @@ public SmartRenameViewModel(
{
_globalOptionService = globalOptionService;
_threadingContext = threadingContext;
_listenerProvider = listenerProvider;
_asyncListener = listenerProvider.GetListener(FeatureAttribute.SmartRename);
_smartRenameSession = smartRenameSession;
_smartRenameSession.PropertyChanged += SessionPropertyChanged;

BaseViewModel = baseViewModel;
BaseViewModel.PropertyChanged += IdentifierTextPropertyChanged;
this.BaseViewModel.IdentifierText = baseViewModel.IdentifierText;
BaseViewModel.PropertyChanged += BaseViewModelPropertyChanged;
BaseViewModel.IdentifierText = baseViewModel.IdentifierText;

SetupTelemetry();

this.SupportsAutomaticSuggestions = _globalOptionService.GetOption(InlineRenameUIOptionsStorage.GetSuggestionsAutomatically);
this.IsUsingSemanticContext = _globalOptionService.GetOption(InlineRenameUIOptionsStorage.GetSuggestionsContext);
// Use existing "CollapseSuggestionsPanel" option (true if user does not wish to get suggestions automatically) to honor user's choice.
this.IsAutomaticSuggestionsEnabled = this.SupportsAutomaticSuggestions && !_globalOptionService.GetOption(InlineRenameUIOptionsStorage.CollapseSuggestionsPanel);
if (this.IsAutomaticSuggestionsEnabled)
Expand All @@ -139,8 +147,7 @@ private void FetchSuggestions(bool isAutomaticOnInitialization)

if (_getSuggestionsTask.Status is TaskStatus.RanToCompletion or TaskStatus.Faulted or TaskStatus.Canceled)
{
var listener = _listenerProvider.GetListener(FeatureAttribute.SmartRename);
var listenerToken = listener.BeginAsyncOperation(nameof(_smartRenameSession.GetSuggestionsAsync));
var listenerToken = _asyncListener.BeginAsyncOperation(nameof(_smartRenameSession.GetSuggestionsAsync));
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_getSuggestionsTask = GetSuggestionsTaskAsync(isAutomaticOnInitialization, _cancellationTokenSource.Token).CompletesAsyncOperation(listenerToken);
Expand All @@ -151,22 +158,47 @@ private async Task GetSuggestionsTaskAsync(bool isAutomaticOnInitialization, Can
{
if (isAutomaticOnInitialization)
{
// ConfigureAwait(true) to stay on the UI thread;
// WPF view is bound to _smartRenameSession properties and so they must be updated on the UI thread.
await Task.Delay(_smartRenameSession.AutomaticFetchDelay, cancellationToken).ConfigureAwait(true);
await Task.Delay(_smartRenameSession.AutomaticFetchDelay, cancellationToken)
.ConfigureAwait(false);
}

if (cancellationToken.IsCancellationRequested || _isDisposed)
{
return;
}
_ = await _smartRenameSession.GetSuggestionsAsync(cancellationToken).ConfigureAwait(true);
return;

if (IsUsingSemanticContext)
{
var document = this.BaseViewModel.Session.TriggerDocument;
var smartRenameContext = ImmutableDictionary<string, string[]>.Empty;
var editorRenameService = document.GetRequiredLanguageService<IEditorInlineRenameService>();
var renameLocations = await this.BaseViewModel.Session.AllRenameLocationsTask.JoinAsync(cancellationToken)
.ConfigureAwait(false);
var context = await editorRenameService.GetRenameContextAsync(this.BaseViewModel.Session.RenameInfo, renameLocations, cancellationToken)
.ConfigureAwait(false);
smartRenameContext = ImmutableDictionary.CreateRange<string, string[]>(
context
.Select(n => new KeyValuePair<string, string[]>(n.Key, n.Value.ToArray())));
_ = await _smartRenameSession.GetSuggestionsAsync(smartRenameContext, cancellationToken)
.ConfigureAwait(false);
}
else
{
_ = await _smartRenameSession.GetSuggestionsAsync(cancellationToken)
.ConfigureAwait(false);
}
}

private void SessionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
_threadingContext.ThrowIfNotOnUIThread();
var listenerToken = _asyncListener.BeginAsyncOperation(nameof(SessionPropertyChanged));
var sessionPropertyChangedTask = SessionPropertyChangedAsync(sender, e).CompletesAsyncOperation(listenerToken);
}

private async Task SessionPropertyChangedAsync(object sender, PropertyChangedEventArgs e)
{
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();

// _smartRenameSession.SuggestedNames is a normal list. We need to convert it to ObservableCollection to bind to UI Element.
if (e.PropertyName == nameof(_smartRenameSession.SuggestedNames))
{
Expand Down Expand Up @@ -226,7 +258,7 @@ public void Dispose()
{
_isDisposed = true;
_smartRenameSession.PropertyChanged -= SessionPropertyChanged;
BaseViewModel.PropertyChanged -= IdentifierTextPropertyChanged;
BaseViewModel.PropertyChanged -= BaseViewModelPropertyChanged;
_smartRenameSession.Dispose();
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
Expand Down Expand Up @@ -260,7 +292,7 @@ public void ToggleOrTriggerSuggestions()
private void NotifyPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));

private void IdentifierTextPropertyChanged(object sender, PropertyChangedEventArgs e)
private void BaseViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(BaseViewModel.IdentifierText))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
Expand All @@ -29,6 +30,7 @@ namespace Microsoft.CodeAnalysis.EditorFeatures.Lightup;
private static readonly Func<object, IReadOnlyList<string>> s_suggestedNamesAccessor;

private static readonly Func<object, CancellationToken, Task<IReadOnlyList<string>>> s_getSuggestionsAsync;
private static readonly Func<object, ImmutableDictionary<string, string[]>, CancellationToken, Task<IReadOnlyList<string>>> s_getSuggestionsAsync_WithContext;
private static readonly Action<object> s_onCancel;
private static readonly Action<object, string> s_onSuccess;

Expand All @@ -47,13 +49,14 @@ static ISmartRenameSessionWrapper()
s_suggestedNamesAccessor = LightupHelpers.CreatePropertyAccessor<object, IReadOnlyList<string>>(s_wrappedType, nameof(SuggestedNames), []);

s_getSuggestionsAsync = LightupHelpers.CreateFunctionAccessor<object, CancellationToken, Task<IReadOnlyList<string>>>(s_wrappedType, nameof(GetSuggestionsAsync), typeof(CancellationToken), SpecializedTasks.EmptyReadOnlyList<string>());
s_getSuggestionsAsync_WithContext = LightupHelpers.CreateFunctionAccessor<object, ImmutableDictionary<string, string[]>, CancellationToken, Task<IReadOnlyList<string>>>(s_wrappedType, nameof(GetSuggestionsAsync), typeof(ImmutableDictionary<string, string[]>), typeof(CancellationToken), SpecializedTasks.EmptyReadOnlyList<string>());
s_onCancel = LightupHelpers.CreateActionAccessor<object>(s_wrappedType, nameof(OnCancel));
s_onSuccess = LightupHelpers.CreateActionAccessor<object, string>(s_wrappedType, nameof(OnSuccess), typeof(string));
}

private ISmartRenameSessionWrapper(object instance)
{
this._instance = instance;
_instance = instance;
}

public TimeSpan AutomaticFetchDelay => s_automaticFetchDelayAccessor(_instance);
Expand Down Expand Up @@ -93,6 +96,9 @@ public static bool IsInstance([NotNullWhen(true)] object? instance)
public Task<IReadOnlyList<string>> GetSuggestionsAsync(CancellationToken cancellationToken)
=> s_getSuggestionsAsync(_instance, cancellationToken);

public Task<IReadOnlyList<string>> GetSuggestionsAsync(ImmutableDictionary<string, string[]> context, CancellationToken cancellationToken)
=> s_getSuggestionsAsync_WithContext(_instance, context, cancellationToken);

public void OnCancel()
=> s_onCancel(_instance);

Expand Down
Loading
Loading