diff --git a/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs b/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs index bbb8adf16055d..05c2b1831864d 100644 --- a/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs +++ b/src/EditorFeatures/CSharp/InlineRename/CSharpEditorInlineRenameService.cs @@ -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; @@ -18,4 +28,135 @@ internal sealed class CSharpEditorInlineRenameService( [ImportMany] IEnumerable refactorNotifyServices, IGlobalOptionService globalOptions) : AbstractEditorInlineRenameService(refactorNotifyServices, globalOptions) { + private const int NumberOfContextLines = 20; + private const int MaxDefinitionCount = 10; + private const int MaxReferenceCount = 50; + + /// + /// Uses semantic information of renamed symbol to produce a map containing contextual information for use in Copilot rename feature + /// + /// Map where key indicates the kind of semantic information, and value is an array of relevant code snippets. + public override async Task>> GetRenameContextAsync( + IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken) + { + using var _1 = PooledHashSet.GetInstance(out var seen); + using var _2 = ArrayBuilder.GetInstance(out var definitions); + using var _3 = ArrayBuilder.GetInstance(out var references); + using var _4 = ArrayBuilder.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(renameDefinition.Document, renameDefinition.SourceSpan, cancellationToken).ConfigureAwait(false) ?? + await TryGetSurroundingNodeSpanAsync(renameDefinition.Document, renameDefinition.SourceSpan, cancellationToken).ConfigureAwait(false); + + // Find documentation comments of definitions + var symbolService = renameDefinition.Document.GetRequiredLanguageService(); + 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(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false) ?? + await TryGetSurroundingNodeSpanAsync(renameLocation.Document, renameLocation.TextSpan, cancellationToken).ConfigureAwait(false) ?? + await TryGetSurroundingNodeSpanAsync(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>(); + 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 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()); + } + } + } } diff --git a/src/EditorFeatures/Core.Wpf/InlineRename/UI/SmartRename/SmartRenameViewModel.cs b/src/EditorFeatures/Core.Wpf/InlineRename/UI/SmartRename/SmartRenameViewModel.cs index b78afe146a442..8b680fae48f6f 100644 --- a/src/EditorFeatures/Core.Wpf/InlineRename/UI/SmartRename/SmartRenameViewModel.cs +++ b/src/EditorFeatures/Core.Wpf/InlineRename/UI/SmartRename/SmartRenameViewModel.cs @@ -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; @@ -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; @@ -65,6 +67,11 @@ internal sealed partial class SmartRenameViewModel : INotifyPropertyChanged, IDi /// public bool IsAutomaticSuggestionsEnabled { get; private set; } + /// + /// Determines whether smart rename gets semantic context to augment the request for suggested names. + /// + public bool IsUsingSemanticContext { get; } + private string? _selectedSuggestedName; /// @@ -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) @@ -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); @@ -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.Empty; + var editorRenameService = document.GetRequiredLanguageService(); + 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( + context + .Select(n => new KeyValuePair(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)) { @@ -226,7 +258,7 @@ public void Dispose() { _isDisposed = true; _smartRenameSession.PropertyChanged -= SessionPropertyChanged; - BaseViewModel.PropertyChanged -= IdentifierTextPropertyChanged; + BaseViewModel.PropertyChanged -= BaseViewModelPropertyChanged; _smartRenameSession.Dispose(); _cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Dispose(); @@ -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)) { diff --git a/src/EditorFeatures/Core.Wpf/Lightup/ISmartRenameSessionWrapper.cs b/src/EditorFeatures/Core.Wpf/Lightup/ISmartRenameSessionWrapper.cs index 50975ec4c9aff..5d511ab60a925 100644 --- a/src/EditorFeatures/Core.Wpf/Lightup/ISmartRenameSessionWrapper.cs +++ b/src/EditorFeatures/Core.Wpf/Lightup/ISmartRenameSessionWrapper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -29,6 +30,7 @@ namespace Microsoft.CodeAnalysis.EditorFeatures.Lightup; private static readonly Func> s_suggestedNamesAccessor; private static readonly Func>> s_getSuggestionsAsync; + private static readonly Func, CancellationToken, Task>> s_getSuggestionsAsync_WithContext; private static readonly Action s_onCancel; private static readonly Action s_onSuccess; @@ -47,13 +49,14 @@ static ISmartRenameSessionWrapper() s_suggestedNamesAccessor = LightupHelpers.CreatePropertyAccessor>(s_wrappedType, nameof(SuggestedNames), []); s_getSuggestionsAsync = LightupHelpers.CreateFunctionAccessor>>(s_wrappedType, nameof(GetSuggestionsAsync), typeof(CancellationToken), SpecializedTasks.EmptyReadOnlyList()); + s_getSuggestionsAsync_WithContext = LightupHelpers.CreateFunctionAccessor, CancellationToken, Task>>(s_wrappedType, nameof(GetSuggestionsAsync), typeof(ImmutableDictionary), typeof(CancellationToken), SpecializedTasks.EmptyReadOnlyList()); s_onCancel = LightupHelpers.CreateActionAccessor(s_wrappedType, nameof(OnCancel)); s_onSuccess = LightupHelpers.CreateActionAccessor(s_wrappedType, nameof(OnSuccess), typeof(string)); } private ISmartRenameSessionWrapper(object instance) { - this._instance = instance; + _instance = instance; } public TimeSpan AutomaticFetchDelay => s_automaticFetchDelayAccessor(_instance); @@ -93,6 +96,9 @@ public static bool IsInstance([NotNullWhen(true)] object? instance) public Task> GetSuggestionsAsync(CancellationToken cancellationToken) => s_getSuggestionsAsync(_instance, cancellationToken); + public Task> GetSuggestionsAsync(ImmutableDictionary context, CancellationToken cancellationToken) + => s_getSuggestionsAsync_WithContext(_instance, context, cancellationToken); + public void OnCancel() => s_onCancel(_instance); diff --git a/src/EditorFeatures/Core.Wpf/Lightup/LightupHelpers.cs b/src/EditorFeatures/Core.Wpf/Lightup/LightupHelpers.cs index 6fda65becc9c9..904d780ed3d64 100644 --- a/src/EditorFeatures/Core.Wpf/Lightup/LightupHelpers.cs +++ b/src/EditorFeatures/Core.Wpf/Lightup/LightupHelpers.cs @@ -352,6 +352,96 @@ public static Func CreateFunctionAccessor(Ty return expression.Compile(); } + /// + /// Generates a compiled accessor method for a property which cannot be bound at compile time. + /// + /// The compile-time type representing the instance on which the property is defined. This + /// may be a superclass of the actual type on which the property is declared if the declaring type is not + /// available at compile time. + /// The compile-time type representing the type of the first argument. This + /// may be a superclass of the actual type of the argument if the declared type is not available at compile + /// time. + /// The compile-time type representing the type of the second argument. This + /// may be a superclass of the actual type of the argument if the declared type is not available at compile + /// time. + /// The compile-type type representing the result of the function. This may be a + /// superclass of the actual return type of the function if the return type is not available at compile + /// time. + /// The runtime time on which the first argument is defined. If this value is null, the runtime + /// time is assumed to not exist, and a fallback accessor returning will be + /// generated. + /// The runtime time on which the second argument is defined. If this value is null, the runtime + /// time is assumed to not exist, and a fallback accessor returning will be + /// generated. + /// The name of the method to access. + /// The value to return if the method is not available at runtime. + /// An accessor method to access the specified runtime property. + public static Func CreateFunctionAccessor(Type? type, string methodName, Type? arg0Type, Type? arg1Type, TResult defaultValue) + { + if (methodName is null) + { + throw new ArgumentNullException(nameof(methodName)); + } + + if (type == null) + { + return CreateFallbackFunction(defaultValue); + } + + if (!typeof(T).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())) + { + throw new InvalidOperationException($"Type '{type}' is not assignable to type '{typeof(T)}'"); + } + + var method = type.GetTypeInfo().GetDeclaredMethods(methodName).Single(method => + { + var parameters = method.GetParameters(); + return parameters is [{ ParameterType: var parameter0Type }, { ParameterType: var parameter1Type }] && parameter0Type == arg0Type && parameter1Type == arg1Type; + }); + + var parameters = method.GetParameters(); + if (arg0Type != parameters[0].ParameterType) + { + throw new ArgumentException($"Type '{arg0Type}' was expected to match parameter type '{parameters[0].ParameterType}'", nameof(arg0Type)); + } + if (arg1Type != parameters[1].ParameterType) + { + throw new ArgumentException($"Type '{arg1Type}' was expected to match parameter type '{parameters[1].ParameterType}'", nameof(arg1Type)); + } + + if (!typeof(TResult).GetTypeInfo().IsAssignableFrom(method.ReturnType.GetTypeInfo())) + { + throw new InvalidOperationException($"Method '{method}' produces a value of type '{method.ReturnType}', which is not assignable to type '{typeof(TResult)}'"); + } + + var parameter = Expression.Parameter(typeof(T), GenerateParameterName(typeof(T))); + var argument0 = Expression.Parameter(typeof(TArg0), parameters[0].Name); + var argument1 = Expression.Parameter(typeof(TArg1), parameters[1].Name); + var instance = + type.GetTypeInfo().IsAssignableFrom(typeof(T).GetTypeInfo()) + ? (Expression)parameter + : Expression.Convert(parameter, type); + var convertedArgument0 = + arg0Type.GetTypeInfo().IsAssignableFrom(typeof(TArg0).GetTypeInfo()) + ? (Expression)argument0 + : Expression.Convert(argument0, arg0Type); + var convertedArgument1 = + arg1Type.GetTypeInfo().IsAssignableFrom(typeof(TArg1).GetTypeInfo()) + ? (Expression)argument1 + : Expression.Convert(argument1, arg1Type); + + var expression = + Expression.Lambda>( + Expression.Convert( + Expression.Call( + instance, + method, + convertedArgument0, convertedArgument1), typeof(TResult)), + parameter, + argument0, argument1); + return expression.Compile(); + } + private static string GenerateParameterName(Type parameterType) { var typeName = parameterType.Name; @@ -438,4 +528,21 @@ TResult FallbackFunction(T instance, TArg arg) return FallbackFunction; } + + private static Func CreateFallbackFunction(TResult defaultValue) + { + TResult FallbackFunction(T instance, TArg0 arg0, TArg1 arg1) + { + if (instance == null) + { + // Unlike an extension method which would throw ArgumentNullException here, the light-up + // behavior needs to match behavior of the underlying property. + throw new NullReferenceException(); + } + + return defaultValue; + } + + return FallbackFunction; + } } diff --git a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs index 417c3d27b7b75..4f88d81441d64 100644 --- a/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/ExternalAccess/VSTypeScript/VSTypeScriptEditorInlineRenameService.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Immutable; using System.Composition; using System.Threading; using System.Threading.Tasks; @@ -10,6 +11,7 @@ using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; using Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript.Api; using Microsoft.CodeAnalysis.Host.Mef; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.ExternalAccess.VSTypeScript; @@ -22,6 +24,11 @@ internal sealed class VSTypeScriptEditorInlineRenameService( { private readonly Lazy? _service = service; + public Task>> GetRenameContextAsync(IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableDictionary>.Empty); + } + public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) { if (_service != null) diff --git a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs index 5534d677149a0..98e318d7a48d8 100644 --- a/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/InlineRename/AbstractEditorInlineRenameService.cs @@ -3,10 +3,12 @@ // See the LICENSE file in the project root for more information. using System.Collections.Generic; +using System.Collections.Immutable; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Rename; +using Microsoft.CodeAnalysis.Text; namespace Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; @@ -30,4 +32,32 @@ public async Task GetRenameInfoAsync(Document document, int p return new SymbolInlineRenameInfo( _refactorNotifyServices, symbolicInfo, _globalOptions.CreateProvider(), cancellationToken); } + + public virtual Task>> GetRenameContextAsync(IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableDictionary>.Empty); + } + + /// + /// Returns the of the nearest encompassing of type + /// of which the supplied is a part within the supplied + /// . + /// + protected static async Task TryGetSurroundingNodeSpanAsync( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + where T : SyntaxNode + { + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + if (root is null) + { + return null; + } + + var containingNode = root.FindNode(textSpan); + var targetNode = containingNode.FirstAncestorOrSelf() ?? containingNode; + + return targetNode.Span; + } } diff --git a/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs b/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs index c1055e59a9df9..ad4d3583fafd5 100644 --- a/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs +++ b/src/EditorFeatures/Core/InlineRename/IEditorInlineRenameService.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Editor.Implementation.InlineRename; using Microsoft.CodeAnalysis.Host; using Microsoft.CodeAnalysis.Rename; using Microsoft.CodeAnalysis.Rename.ConflictEngine; @@ -253,5 +254,20 @@ internal interface IInlineRenameInfo /// internal interface IEditorInlineRenameService : ILanguageService { + /// + /// Returns necessary to establish the inline rename session. + /// Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken); + + /// + /// Returns optional context used in Copilot addition to inline rename feature. + /// + /// + /// + /// + /// + Task>> GetRenameContextAsync( + IInlineRenameInfo inlineRenameInfo, + IInlineRenameLocationSet inlineRenameLocationSet, + CancellationToken cancellationToken); } diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs index 4f30186d98c73..2e44682ac8a6a 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameSession.cs @@ -49,7 +49,6 @@ internal partial class InlineRenameSession : IInlineRenameSession, IFeatureContr private readonly IEnumerable _refactorNotifyServices; private readonly IAsynchronousOperationListener _asyncListener; private readonly Solution _baseSolution; - private readonly Document _triggerDocument; private readonly ITextView _triggerView; private readonly IDisposable _inlineRenameSessionDurationLogBlock; private readonly IThreadingContext _threadingContext; @@ -62,6 +61,11 @@ internal partial class InlineRenameSession : IInlineRenameSession, IFeatureContr private bool _previewChanges; private readonly Dictionary _openTextBuffers = []; + /// + /// The original where rename was triggered + /// + public Document TriggerDocument { get; } + /// /// The original for the identifier that rename was triggered on /// @@ -90,6 +94,17 @@ private set /// public InlineRenameFileRenameInfo FileRenameInfo { get; } + /// + /// Information about this rename session. + /// + public IInlineRenameInfo RenameInfo => _renameInfo; + + /// + /// The task which computes the main rename locations against the original workspace + /// snapshot. + /// + public JoinableTask AllRenameLocationsTask => _allRenameLocationsTask; + /// /// Keep-alive session held alive with the OOP server. This allows us to pin the initial solution snapshot over on /// the oop side, which is valuable for preventing it from constantly being dropped/synced on every conflict @@ -148,8 +163,8 @@ public InlineRenameSession( _renameInfo = renameInfo; TriggerSpan = triggerSpan; - _triggerDocument = triggerSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); - if (_triggerDocument == null) + TriggerDocument = triggerSpan.Snapshot.GetOpenDocumentInCurrentContextWithChanges(); + if (TriggerDocument == null) { throw new InvalidOperationException(EditorFeaturesResources.The_triggerSpan_is_not_included_in_the_given_workspace); } @@ -180,7 +195,7 @@ public InlineRenameSession( _initialRenameText = triggerSpan.GetText(); this.ReplacementText = _initialRenameText; - _baseSolution = _triggerDocument.Project.Solution; + _baseSolution = TriggerDocument.Project.Solution; this.UndoManager = workspace.Services.GetService(); FileRenameInfo = _renameInfo.GetFileRenameInfo(); @@ -824,7 +839,7 @@ private async Task CommitCoreAsync(IUIThreadOperationContext operationContext, b _renameInfo.FullDisplayName, _renameInfo.Glyph, newSolution, - _triggerDocument.Project.Solution); + TriggerDocument.Project.Solution); if (newSolution == null) { diff --git a/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs b/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs index 4e49a010b802e..7ec8df35832b9 100644 --- a/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs +++ b/src/EditorFeatures/Core/InlineRename/InlineRenameUIOptionsStorage.cs @@ -12,4 +12,5 @@ internal sealed class InlineRenameUIOptionsStorage public static readonly Option2 CollapseUI = new("dotnet_collapse_inline_rename_ui", defaultValue: false); public static readonly Option2 CollapseSuggestionsPanel = new("dotnet_collapse_suggestions_in_inline_rename_ui", defaultValue: false); public static readonly Option2 GetSuggestionsAutomatically = new("dotnet_rename_get_suggestions_automatically", defaultValue: false); + public static readonly Option2 GetSuggestionsContext = new("visual_studio_enable_copilot_rename_context", defaultValue: false); } diff --git a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs index 75e0e4065feab..f2b5c83e82a21 100644 --- a/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs +++ b/src/VisualStudio/Core/Def/Options/VisualStudioOptionStorage.cs @@ -373,6 +373,7 @@ public bool TryFetch(LocalUserRegistryOptionPersister persister, OptionKey2 opti {"dotnet_report_invalid_json_patterns", new RoamingProfileStorage("TextEditor.%LANGUAGE%.Specific.ReportInvalidJsonPatterns")}, {"visual_studio_enable_key_binding_reset", new FeatureFlagStorage("Roslyn.KeybindingResetEnabled")}, {"visual_studio_enable_semantic_search", new FeatureFlagStorage("Roslyn.SemanticSearchEnabled")}, + {"visual_studio_enable_copilot_rename_context", new FeatureFlagStorage("Roslyn.CopilotRenameGetContext")}, {"visual_studio_key_binding_needs_reset", new LocalUserProfileStorage(@"Roslyn\Internal\KeybindingsStatus", "NeedsReset")}, {"visual_studio_key_binding_reset_never_show_again", new LocalUserProfileStorage(@"Roslyn\Internal\KeybindingsStatus", "NeverShowAgain")}, {"visual_studio_resharper_key_binding_status", new LocalUserProfileStorage(@"Roslyn\Internal\KeybindingsStatus", "ReSharperStatus")}, diff --git a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef index 25767a4f03745..aaf0e6d907cd4 100644 --- a/src/VisualStudio/Core/Def/PackageRegistration.pkgdef +++ b/src/VisualStudio/Core/Def/PackageRegistration.pkgdef @@ -45,6 +45,12 @@ "Title"="Enable C# Semantic Search" "PreviewPaneChannels"="IntPreview,int.main" +[$RootKey$\FeatureFlags\Roslyn\CopilotRenameGetContext] +"Description"="Add semantic context to Copilot Rename Suggestions in C#." +"Value"=dword:00000000 +"Title"="Semantic Context in C# Copilot Rename (requires restart)" +"PreviewPaneChannels"="IntPreview,int.main" + // Corresponds to WellKnownExperimentNames.LspPullDiagnosticsFeatureFlag [$RootKey$\FeatureFlags\Lsp\PullDiagnostics] "Description"="Enables the LSP-powered diagnostics for managed .Net projects" diff --git a/src/VisualStudio/ExternalAccess/FSharp/Internal/Editor/FSharpEditorInlineRenameService.cs b/src/VisualStudio/ExternalAccess/FSharp/Internal/Editor/FSharpEditorInlineRenameService.cs index d2ed503b29202..e6c8670ed7732 100644 --- a/src/VisualStudio/ExternalAccess/FSharp/Internal/Editor/FSharpEditorInlineRenameService.cs +++ b/src/VisualStudio/ExternalAccess/FSharp/Internal/Editor/FSharpEditorInlineRenameService.cs @@ -5,10 +5,10 @@ #nullable disable using System; -using System.Linq; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis.Editor; @@ -202,6 +202,11 @@ public FSharpEditorInlineRenameService( _service = service; } + public Task>> GetRenameContextAsync(IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableDictionary>.Empty); + } + public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) { #pragma warning disable CS0612 // Type or member is obsolete diff --git a/src/VisualStudio/Xaml/Impl/Features/InlineRename/XamlEditorInlineRenameService.cs b/src/VisualStudio/Xaml/Impl/Features/InlineRename/XamlEditorInlineRenameService.cs index 618fd9df2be78..215018e9044bd 100644 --- a/src/VisualStudio/Xaml/Impl/Features/InlineRename/XamlEditorInlineRenameService.cs +++ b/src/VisualStudio/Xaml/Impl/Features/InlineRename/XamlEditorInlineRenameService.cs @@ -30,6 +30,11 @@ public XamlEditorInlineRenameService(IXamlRenameInfoService renameService) _renameService = renameService; } + public Task>> GetRenameContextAsync(IInlineRenameInfo inlineRenameInfo, IInlineRenameLocationSet inlineRenameLocationSet, CancellationToken cancellationToken) + { + return Task.FromResult(ImmutableDictionary>.Empty); + } + public async Task GetRenameInfoAsync(Document document, int position, CancellationToken cancellationToken) { var renameInfo = await _renameService.GetRenameInfoAsync(document, position, cancellationToken).ConfigureAwait(false);