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

Smart Rename: Provide additional context for renamed symbol #73873

Merged
merged 55 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
3004167
work in progress on getting context and almost passing it to smart re…
AmadeusW Jun 6, 2024
797896b
pass context to SmartRenameSession
AmadeusW Jun 6, 2024
f306f05
attempt to set PromptOverride through Lightup
AmadeusW Jun 6, 2024
dc2c255
reflection refinement
AmadeusW Jun 6, 2024
3cba7f9
Fix CreatePropertySetter in LightupHelpers
Cosifne Jun 6, 2024
e03d4b8
Find definitions and references and add them to the context
AmadeusW Jun 12, 2024
3516915
also capture snippet of surrounding method
AmadeusW Jun 12, 2024
00a88ec
simplify
AmadeusW Jun 12, 2024
04f32e4
fix GetRequiredLanguageService
AmadeusW Jun 12, 2024
8b3f739
undo code cleanup artifacts
AmadeusW Jun 12, 2024
14f4c21
use ImmutableArray throughout Roslyn's interfaces
AmadeusW Jun 12, 2024
9a664db
fixup
AmadeusW Jun 12, 2024
2eb5b30
refactor to get context only if smart rename is active
AmadeusW Jun 18, 2024
6b3e0a4
revert the now unnecessary change
AmadeusW Jun 18, 2024
3217dbe
restore CreateFunctionAccessor with 2 parameters
AmadeusW Jun 18, 2024
e6c0de8
Dont duplicate work of getting rename locations
AmadeusW Jun 18, 2024
7b3c5d2
use interface in the method declarations
AmadeusW Jun 18, 2024
0aced56
fix warning
AmadeusW Jun 21, 2024
5f483e9
remove IInlineRenameSession from the API
AmadeusW Jun 21, 2024
7d6f4ea
cleanup
AmadeusW Jun 21, 2024
fc7c0c7
refactor getting doccomments
AmadeusW Jun 21, 2024
8e7719a
fixup
AmadeusW Jun 21, 2024
5abd3f1
PR feedback
AmadeusW Jun 24, 2024
13e1afb
Add IEditorInlineRenameService.IsRenameContextSupported
AmadeusW Jun 24, 2024
c13c161
revert unnecessary change
AmadeusW Jun 24, 2024
a342e0a
remove unnecessary changes
AmadeusW Jun 24, 2024
1ff97d8
doc comments
AmadeusW Jun 24, 2024
d0ef8d8
expose TriggerDocument instead of creating a property that returns a …
AmadeusW Jun 24, 2024
810ef6e
add feature flag to control whether we're getting context
AmadeusW Jun 27, 2024
2fe72e4
Merge branch 'dev/amwieczo/getContextForRenamedSymbol' of https://git…
AmadeusW Jun 27, 2024
3eed4a2
Merge branch 'main' into dev/amwieczo/getContextForRenamedSymbol
AmadeusW Jun 27, 2024
e6e6fec
improve event handler name
AmadeusW Jun 27, 2024
31770e1
fix Correctness_Analyzers warnings
AmadeusW Jun 27, 2024
02a5426
update Feature Flag look
AmadeusW Jun 27, 2024
1bc796a
fix Freeing Twice assert
AmadeusW Jun 27, 2024
064eb2e
add 'requires restart' to the FF name
AmadeusW Jun 27, 2024
0c0c2ea
cleanup usings
AmadeusW Jun 27, 2024
121c396
extract constants
AmadeusW Jun 27, 2024
f022adf
remove unused code
AmadeusW Jun 27, 2024
bbe2e44
NumberOfContextLines const
AmadeusW Jul 1, 2024
c30caf3
GetRenameContextCoreAsync doc comments
AmadeusW Jul 1, 2024
bf35972
pr feedback on CSharpEditorInlineRenameService
AmadeusW Jul 1, 2024
c8788ae
override fixes
AmadeusW Jul 1, 2024
028dd58
enforce hard limits on amount of symbols we traverse
AmadeusW Jul 1, 2024
8690915
Get context off UI thread, respond to property changed on UI thread
AmadeusW Jul 1, 2024
171b026
undo unwanted file
AmadeusW Jul 3, 2024
f4bc198
Remove CSharpRenameContextHelper; improve the provided context
AmadeusW Jul 9, 2024
f3c9467
modifiers and doc comments
AmadeusW Jul 9, 2024
6c06fa3
remove never taken branch
AmadeusW Jul 9, 2024
15d2f2d
Merge branch 'main' into dev/amwieczo/getContextForRenamedSymbol
AmadeusW Jul 9, 2024
1faf717
Remove IsRenameContextSupported
AmadeusW Jul 10, 2024
c3f3030
PR feedback to move TryGetSurroundingNodeSpanAsync to parent class
AmadeusW Jul 12, 2024
f14318a
use CompletesAsyncOperation pattern for SessionPropertyChanged
AmadeusW Jul 15, 2024
3a33f76
Create IAsynchronousOperationListener in the constructor
AmadeusW Jul 16, 2024
3705723
improve algorithm that provides context snippet
AmadeusW Jul 16, 2024
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,20 @@

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 @@ -16,4 +27,108 @@ namespace Microsoft.CodeAnalysis.Editor.CSharp.InlineRename;
internal sealed class CSharpEditorInlineRenameService([ImportMany] IEnumerable<IRefactorNotifyService> refactorNotifyServices)
: AbstractEditorInlineRenameService(refactorNotifyServices)
{
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(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we really don't like doing this. it makes this work lang specific (it will only work for C#/VB).

Instead, we should make it so that whatever data is neeeded can be optionally returned by some non-lang-specific ILanguageService that JS/TS/XAML/F# can all implement.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok. this was already designed to let the other langs play. but we should at least expose this for C#/VB. Our policy is that we do not specialize for C# unless it's a C#-specific feature (like for a literal lang feature that VB doesn't have). Otherwise, we light up both.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as there is practically nothing lang-specific here, that should be very easy to do (and we can help with that).

Copy link
Member

@Cosifne Cosifne Jul 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well now I think it is because the copied [CSharpRenameContextHelper.cs](https://github.com/dotnet/roslyn/pull/73873/files#diff-5d446a8c6ebf9f7cfce4e0ff445c83babf4e658cc97c3c989d81bc330dc5e41d) only provide C# implementation and VB is not provided.

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) ??
AmadeusW marked this conversation as resolved.
Show resolved Hide resolved
await TryGetSurroundingNodeSpanAsync<StatementSyntax>(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false);
AmadeusW marked this conversation as resolved.
Show resolved Hide resolved

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 a well defined surrounding span was not computed or if the computed surrounding span was too large,
// select a span that encompasses NumberOfContextLines lines above and NumberOfContextLines lines below the identifier.
if (surroundingSpanOfInterest is null || lineCount <= 0 || lineCount > NumberOfContextLines * 2)
{
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());
AmadeusW marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
}
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 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 @@ -114,12 +121,13 @@ public SmartRenameViewModel(
_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 Down Expand Up @@ -151,43 +159,65 @@ 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.
AmadeusW marked this conversation as resolved.
Show resolved Hide resolved
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();
// _smartRenameSession.SuggestedNames is a normal list. We need to convert it to ObservableCollection to bind to UI Element.
if (e.PropertyName == nameof(_smartRenameSession.SuggestedNames))
_ = _threadingContext.JoinableTaskFactory.RunAsync(async () =>
AmadeusW marked this conversation as resolved.
Show resolved Hide resolved
{
var textInputBackup = BaseViewModel.IdentifierText;
await _threadingContext.JoinableTaskFactory.SwitchToMainThreadAsync();

SuggestedNames.Clear();
// Set limit of 3 results
foreach (var name in _smartRenameSession.SuggestedNames.Take(3))
// _smartRenameSession.SuggestedNames is a normal list. We need to convert it to ObservableCollection to bind to UI Element.
if (e.PropertyName == nameof(_smartRenameSession.SuggestedNames))
{
SuggestedNames.Add(name);
}
var textInputBackup = BaseViewModel.IdentifierText;

// Changing the list may have changed the text in the text box. We need to restore it.
BaseViewModel.IdentifierText = textInputBackup;
SuggestedNames.Clear();
// Set limit of 3 results
foreach (var name in _smartRenameSession.SuggestedNames.Take(3))
{
SuggestedNames.Add(name);
}

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSuggestionsPanelExpanded)));
return;
}
// Changing the list may have changed the text in the text box. We need to restore it.
BaseViewModel.IdentifierText = textInputBackup;

PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSuggestionsPanelExpanded)));
return;
}

// For the rest of the property, like HasSuggestions, IsAvailable and etc. Just forward it has changed to subscriber
PropertyChanged?.Invoke(this, e);
// For the rest of the property, like HasSuggestions, IsAvailable and etc. Just forward it has changed to subscriber
PropertyChanged?.Invoke(this, e);
});
}

public string? ScrollSuggestions(string currentIdentifier, bool down)
Expand Down Expand Up @@ -226,7 +256,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 +290,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