diff --git a/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs b/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs index 794e3d76f4e9d3decaf5d4f93a0f372a71f32bd9..a37e3cb6ddb338cec4c9a4f9bebeb766c8380f01 100644 --- a/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs +++ b/src/EditorFeatures/CSharpTest/ExtractMethod/ExtractMethodTests.cs @@ -10329,7 +10329,7 @@ public async Task ExtractMethod_Argument1() var service = new CSharpExtractMethodService(); Assert.NotNull(await Record.ExceptionAsync(async () => { - var tree = await service.ExtractMethodAsync(null, default, null, CancellationToken.None); + var tree = await service.ExtractMethodAsync(null, default, false, null, CancellationToken.None); })); } diff --git a/src/EditorFeatures/Core/Implementation/ExtractMethod/AbstractExtractMethodCommandHandler.cs b/src/EditorFeatures/Core/Implementation/ExtractMethod/AbstractExtractMethodCommandHandler.cs index 9f7e6c502b0e840f2ce694690f22cf1371ca267d..7bc24e754784d9961df7e332ab632ee2e1f1cce4 100644 --- a/src/EditorFeatures/Core/Implementation/ExtractMethod/AbstractExtractMethodCommandHandler.cs +++ b/src/EditorFeatures/Core/Implementation/ExtractMethod/AbstractExtractMethodCommandHandler.cs @@ -220,7 +220,7 @@ private bool TryNotifyFailureToUser(Document document, ExtractMethodResult resul { options = options.WithChangedOption(ExtractMethodOptions.DontPutOutOrRefOnStruct, document.Project.Language, true); var newResult = ExtractMethodService.ExtractMethodAsync( - document, spans.Single().Span.ToTextSpan(), options, cancellationToken).WaitAndGetResult(cancellationToken); + document, spans.Single().Span.ToTextSpan(), false, options, cancellationToken).WaitAndGetResult(cancellationToken); // retry succeeded, return new result if (newResult.Succeeded || newResult.SucceededWithSuggestion) diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpExtractMethodService.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpExtractMethodService.cs index d87fde6c4aecf35f6bef00a9af369c0c5e478bc3..e565ab19d5d624f7e8aa09f9e80367a4ecb96f8d 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpExtractMethodService.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpExtractMethodService.cs @@ -22,9 +22,9 @@ protected override CSharpSelectionValidator CreateSelectionValidator(SemanticDoc return new CSharpSelectionValidator(document, textSpan, options); } - protected override CSharpMethodExtractor CreateMethodExtractor(CSharpSelectionResult selectionResult) + protected override CSharpMethodExtractor CreateMethodExtractor(CSharpSelectionResult selectionResult, bool extractLocalFunction) { - return new CSharpMethodExtractor(selectionResult); + return new CSharpMethodExtractor(selectionResult, extractLocalFunction); } } } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs index c4bf5e7ecd0fb34a0696c82ab26624b5a8d08230..63a4868b2d87e81d7494b9baa248aeff3dddf07e 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.ExpressionCodeGenerator.cs @@ -21,8 +21,9 @@ private class ExpressionCodeGenerator : CSharpCodeGenerator public ExpressionCodeGenerator( InsertionPoint insertionPoint, SelectionResult selectionResult, - AnalyzerResult analyzerResult) - : base(insertionPoint, selectionResult, analyzerResult) + AnalyzerResult analyzerResult, + bool extractLocalFunction) + : base(insertionPoint, selectionResult, analyzerResult, extractLocalFunction) { } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs index 60fe44599934f967d63cfd68c984b946a264b677..0dd8352d9f45e9d93e9b860ead30515efd5a9abf 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.MultipleStatementsCodeGenerator.cs @@ -18,8 +18,9 @@ public class MultipleStatementsCodeGenerator : CSharpCodeGenerator public MultipleStatementsCodeGenerator( InsertionPoint insertionPoint, SelectionResult selectionResult, - AnalyzerResult analyzerResult) - : base(insertionPoint, selectionResult, analyzerResult) + AnalyzerResult analyzerResult, + bool extractLocalFunction) + : base(insertionPoint, selectionResult, analyzerResult, extractLocalFunction) { } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs index 2b3d6286a85a155fb8c695923e0ec29b47a62965..c56fb124dc3b00ef6240a918010fe1b3131daf6f 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.SingleStatementCodeGenerator.cs @@ -18,8 +18,9 @@ public class SingleStatementCodeGenerator : CSharpCodeGenerator public SingleStatementCodeGenerator( InsertionPoint insertionPoint, SelectionResult selectionResult, - AnalyzerResult analyzerResult) - : base(insertionPoint, selectionResult, analyzerResult) + AnalyzerResult analyzerResult, + bool extractLocalFunction) + : base(insertionPoint, selectionResult, analyzerResult, extractLocalFunction) { } diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs index 8f53937edf1bd8a5685d386776fc8da6f1a77154..58b7d26808715cb4edc468f2ca5167de17d66685 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.CSharpCodeGenerator.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; @@ -32,30 +33,32 @@ private abstract partial class CSharpCodeGenerator : CodeGenerator("Unknown selection"); @@ -64,8 +67,9 @@ private abstract partial class CSharpCodeGenerator : CodeGenerator CreateGeneratedCodeAsync(OperationS // here, we explicitly insert newline at the end of "{" of auto generated method decl so that anchor knows how to find out // indentation of inserted statements (from users code) with user code style preserved var root = newDocument.Root; - var methodDefinition = root.GetAnnotatedNodes(this.MethodDefinitionAnnotation).First(); - - var newMethodDefinition = TweakNewLinesInMethod(methodDefinition); - - newDocument = await newDocument.WithSyntaxRootAsync( - root.ReplaceNode(methodDefinition, newMethodDefinition), cancellationToken).ConfigureAwait(false); + var methodDefinition = root.GetAnnotatedNodes(this.MethodDefinitionAnnotation).First(); + if (methodDefinition is LocalFunctionStatementSyntax localMethod) + { + var newMethodDefinition = TweakNewLinesInMethod(localMethod); + newDocument = await newDocument.WithSyntaxRootAsync( + root.ReplaceNode(methodDefinition, newMethodDefinition), cancellationToken).ConfigureAwait(false); + } + else if (methodDefinition is MethodDeclarationSyntax method) + { + var newMethodDefinition = TweakNewLinesInMethod(method); + newDocument = await newDocument.WithSyntaxRootAsync( + root.ReplaceNode(methodDefinition, newMethodDefinition), cancellationToken).ConfigureAwait(false); + } + else + { + throw new NotSupportedException("methodDefinition expected to be a regular or local method."); + } } return await base.CreateGeneratedCodeAsync(status, newDocument, cancellationToken).ConfigureAwait(false); @@ -642,6 +657,28 @@ private static MethodDeclarationSyntax TweakNewLinesInMethod(MethodDeclarationSy } } + private static LocalFunctionStatementSyntax TweakNewLinesInMethod(LocalFunctionStatementSyntax methodDefinition) + { + if (methodDefinition.Body != null) + { + return methodDefinition.ReplaceToken( + methodDefinition.Body.OpenBraceToken, + methodDefinition.Body.OpenBraceToken.WithAppendedTrailingTrivia( + SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); + } + else if (methodDefinition.ExpressionBody != null) + { + return methodDefinition.ReplaceToken( + methodDefinition.ExpressionBody.ArrowToken, + methodDefinition.ExpressionBody.ArrowToken.WithPrependedLeadingTrivia( + SpecializedCollections.SingletonEnumerable(SyntaxFactory.ElasticCarriageReturnLineFeed))); + } + else + { + return methodDefinition; + } + } + protected StatementSyntax GetStatementContainingInvocationToExtractedMethodWorker() { var callSignature = CreateCallSignature(); diff --git a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs index 93466a43d1401b271727cb0997bf803270e1e284..dea102fe8af2d1cb2f1cb7a0db7f47b66b69902c 100644 --- a/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs +++ b/src/Features/CSharp/Portable/ExtractMethod/CSharpMethodExtractor.cs @@ -17,8 +17,8 @@ namespace Microsoft.CodeAnalysis.CSharp.ExtractMethod { internal partial class CSharpMethodExtractor : MethodExtractor { - public CSharpMethodExtractor(CSharpSelectionResult result) - : base(result) + public CSharpMethodExtractor(CSharpSelectionResult result, bool extractLocalFunction = false) + : base(result, extractLocalFunction) { } @@ -67,9 +67,9 @@ protected override async Task ExpandAsync(SelectionResult sele return await selection.SemanticDocument.WithSyntaxRootAsync(selection.SemanticDocument.Root.ReplaceNode(lastExpression, newExpression), cancellationToken).ConfigureAwait(false); } - protected override Task GenerateCodeAsync(InsertionPoint insertionPoint, SelectionResult selectionResult, AnalyzerResult analyzeResult, CancellationToken cancellationToken) + protected override Task GenerateCodeAsync(InsertionPoint insertionPoint, SelectionResult selectionResult, AnalyzerResult analyzeResult, bool extractLocalFunction, CancellationToken cancellationToken) { - return CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, cancellationToken); + return CSharpCodeGenerator.GenerateAsync(insertionPoint, selectionResult, analyzeResult, extractLocalFunction, cancellationToken); } protected override IEnumerable GetFormattingRules(Document document) diff --git a/src/Features/Core/Portable/CodeRefactorings/ExtractLocalMethod/AbstractExtractLocalMethodCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ExtractLocalMethod/AbstractExtractLocalMethodCodeRefactoringProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..6a4e7cf933228b76ef92ba5d7dc654304ff67d53 --- /dev/null +++ b/src/Features/Core/Portable/CodeRefactorings/ExtractLocalMethod/AbstractExtractLocalMethodCodeRefactoringProvider.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeRefactorings.ExtractMethod; +using Microsoft.CodeAnalysis.ExtractMethod; +using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CodeRefactorings.ExtractLocalMethod +{ + [ExportCodeRefactoringProvider(LanguageNames.CSharp, + Name = PredefinedCodeRefactoringProviderNames.ExtractLocalMethod), Shared] + internal class ExtractLocalMethodCodeRefactoringProvider : ExtractMethodCodeRefactoringProvider + { + [ImportingConstructor] + public ExtractLocalMethodCodeRefactoringProvider() + { + } + + internal override async Task<(CodeAction action, string methodBlock)> GetCodeActionAsync( + Document document, + TextSpan textSpan, + CancellationToken cancellationToken) + { + var result = await ExtractMethodService.ExtractMethodAsync( + document, + textSpan, + extractLocalMethod: true, + cancellationToken: cancellationToken).ConfigureAwait(false); + Contract.ThrowIfNull(result); + + if (result.Succeeded || result.SucceededWithSuggestion) + { + var documentOptions = await document.GetOptionsAsync(cancellationToken).ConfigureAwait(false); + var description = documentOptions.GetOption(ExtractMethodOptions.AllowMovingDeclaration) ? + FeaturesResources.Extract_Local_Method_plus_Local : FeaturesResources.Extract_Local_Method; + + var codeAction = new MyCodeAction(description, c => AddRenameAnnotationAsync(result.Document, result.InvocationNameToken, c)); + var methodBlock = result.MethodDeclarationNode; + + return (codeAction, methodBlock.ToString()); + } + + return default; + } + } +} diff --git a/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs b/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs index 6f55afa07695e28c9c9e71352af74fd078ddcdaa..606f7e124042a6b9798c21a13ad29f7d6bef35cf 100644 --- a/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/CodeRefactorings/ExtractMethod/AbstractExtractMethodCodeRefactoringProvider.cs @@ -55,7 +55,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte context.RegisterRefactoring(action.action, textSpan); } - private async Task<(CodeAction action, string methodBlock)> GetCodeActionAsync( + internal virtual async Task<(CodeAction action, string methodBlock)> GetCodeActionAsync( Document document, TextSpan textSpan, CancellationToken cancellationToken) @@ -81,7 +81,7 @@ public override async Task ComputeRefactoringsAsync(CodeRefactoringContext conte return default; } - private async Task AddRenameAnnotationAsync(Document document, SyntaxToken invocationNameToken, CancellationToken cancellationToken) + internal async Task AddRenameAnnotationAsync(Document document, SyntaxToken invocationNameToken, CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); @@ -92,7 +92,7 @@ private async Task AddRenameAnnotationAsync(Document document, SyntaxT return document.WithSyntaxRoot(finalRoot); } - private class MyCodeAction : CodeAction.DocumentChangeAction + internal class MyCodeAction : CodeAction.DocumentChangeAction { public MyCodeAction(string title, Func> createChangedDocument) : base(title, createChangedDocument) diff --git a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs index 1d06ba94c0bc359ea970b77f4eceb0f9efc481db..6be96e60088fbffae33a4349c961d78cee5409a3 100644 --- a/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs +++ b/src/Features/Core/Portable/CodeRefactorings/PredefinedCodeRefactoringProviderNames.cs @@ -14,6 +14,7 @@ internal static class PredefinedCodeRefactoringProviderNames public const string ConvertTupleToStruct = "Convert Tuple to Struct Code Action Provider"; public const string EncapsulateField = "Encapsulate Field"; public const string ExtractInterface = "Extract Interface Code Action Provider"; + public const string ExtractLocalMethod = "Extract Local Method Code Action Provider"; public const string ExtractMethod = "Extract Method Code Action Provider"; public const string GenerateConstructorFromMembers = "Generate Constructor From Members Code Action Provider"; public const string GenerateDefaultConstructors = "Generate Default Constructors Code Action Provider"; diff --git a/src/Features/Core/Portable/ExtractMethod/AbstractExtractMethodService.cs b/src/Features/Core/Portable/ExtractMethod/AbstractExtractMethodService.cs index c82cef0e1b507ecb6a7054084ec9d048c7b7ce9f..5272791334741afe1bfc53880171b6421c4157ec 100644 --- a/src/Features/Core/Portable/ExtractMethod/AbstractExtractMethodService.cs +++ b/src/Features/Core/Portable/ExtractMethod/AbstractExtractMethodService.cs @@ -13,11 +13,12 @@ internal abstract class AbstractExtractMethodService ExtractMethodAsync( Document document, TextSpan textSpan, + bool extractLocalFunction, OptionSet options, CancellationToken cancellationToken) { @@ -36,7 +37,7 @@ internal abstract class AbstractExtractMethodService ExtractMethodAsync(Document document, TextSpan textSpan, OptionSet options = null, CancellationToken cancellationToken = default) + public static Task ExtractMethodAsync(Document document, TextSpan textSpan, bool extractLocalMethod = false, OptionSet options = null, CancellationToken cancellationToken = default) { - return document.GetLanguageService().ExtractMethodAsync(document, textSpan, options, cancellationToken); + return document.GetLanguageService().ExtractMethodAsync(document, textSpan, extractLocalMethod, options, cancellationToken); } } } diff --git a/src/Features/Core/Portable/ExtractMethod/IExtractMethodService.cs b/src/Features/Core/Portable/ExtractMethod/IExtractMethodService.cs index 627ff0f55891b533a9e31b0c15ed9ad4e12a6d27..8ca6035555ba53d6e06bd58cd562861bc8aacd4a 100644 --- a/src/Features/Core/Portable/ExtractMethod/IExtractMethodService.cs +++ b/src/Features/Core/Portable/ExtractMethod/IExtractMethodService.cs @@ -10,6 +10,6 @@ namespace Microsoft.CodeAnalysis.ExtractMethod { internal interface IExtractMethodService : ILanguageService { - Task ExtractMethodAsync(Document document, TextSpan textSpan, OptionSet options = null, CancellationToken cancellationToken = default); + Task ExtractMethodAsync(Document document, TextSpan textSpan, bool extractLocalFunction = false, OptionSet options = null, CancellationToken cancellationToken = default); } } diff --git a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.CodeGenerator.cs b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.CodeGenerator.cs index 2735f2010980aa202401de16b9419ebab5e9b112..dfac9f617ea0b5ae006bc81c617a1d12b9296df7 100644 --- a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.CodeGenerator.cs +++ b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.CodeGenerator.cs @@ -31,7 +31,9 @@ protected abstract partial class CodeGenerator GenerateAsync(CancellationToken cancellationTok var newCallSiteRoot = callSiteDocument.Root; var previousMemberNode = GetPreviousMember(callSiteDocument); - // it is possible in a script file case where there is no previous member. in that case, insert new text into top level script - var destination = (previousMemberNode.Parent == null) ? previousMemberNode : previousMemberNode.Parent; + SyntaxNode destination; + if (ExtractLocalFunction) + { + destination = InsertionPoint.With(callSiteDocument).GetContext(); + } + else + { + // it is possible in a script file case where there is no previous member. in that case, insert new text into top level script + destination = (previousMemberNode.Parent == null) ? previousMemberNode : previousMemberNode.Parent; + } var codeGenerationService = SemanticDocument.Document.GetLanguageService(); var result = GenerateMethodDefinition(cancellationToken); - var newContainer = codeGenerationService.AddMethod( - destination, result.Data, - new CodeGenerationOptions(afterThisLocation: previousMemberNode.GetLocation(), generateDefaultAccessibility: true, generateMethodBodies: true), - cancellationToken); + + SyntaxNode newContainer; + if (ExtractLocalFunction) + { + newContainer = codeGenerationService.AddMethod( + destination, result.Data, + new CodeGenerationOptions(generateDefaultAccessibility: false), + cancellationToken); + } + else + { + newContainer = codeGenerationService.AddMethod( + destination, result.Data, + new CodeGenerationOptions(afterThisLocation: previousMemberNode.GetLocation(), generateDefaultAccessibility: true, generateMethodBodies: true), + cancellationToken); + } var newDocument = callSiteDocument.Document.WithSyntaxRoot(newCallSiteRoot.ReplaceNode(destination, newContainer)); newDocument = await Simplifier.ReduceAsync(newDocument, Simplifier.Annotation, null, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs index b81ce2e36101810e99ef61587957f4a5ba500166..089f0926f3f56cddb7133df4e946c1d1a7764e4f 100644 --- a/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs +++ b/src/Features/Core/Portable/ExtractMethod/MethodExtractor.cs @@ -14,11 +14,13 @@ namespace Microsoft.CodeAnalysis.ExtractMethod internal abstract partial class MethodExtractor { protected readonly SelectionResult OriginalSelectionResult; + protected readonly bool ExtractLocalFunction; - public MethodExtractor(SelectionResult selectionResult) + public MethodExtractor(SelectionResult selectionResult, bool extractLocalFunction = false) { Contract.ThrowIfNull(selectionResult); OriginalSelectionResult = selectionResult; + ExtractLocalFunction = extractLocalFunction; } protected abstract Task AnalyzeAsync(SelectionResult selectionResult, CancellationToken cancellationToken); @@ -26,7 +28,7 @@ public MethodExtractor(SelectionResult selectionResult) protected abstract Task PreserveTriviaAsync(SelectionResult selectionResult, CancellationToken cancellationToken); protected abstract Task ExpandAsync(SelectionResult selection, CancellationToken cancellationToken); - protected abstract Task GenerateCodeAsync(InsertionPoint insertionPoint, SelectionResult selectionResult, AnalyzerResult analyzeResult, CancellationToken cancellationToken); + protected abstract Task GenerateCodeAsync(InsertionPoint insertionPoint, SelectionResult selectionResult, AnalyzerResult analyzeResult, bool extractLocalFunction, CancellationToken cancellationToken); protected abstract SyntaxToken GetMethodNameAtInvocation(IEnumerable methodNames); protected abstract IEnumerable GetFormattingRules(Document document); @@ -58,6 +60,7 @@ public async Task ExtractMethodAsync(CancellationToken canc insertionPoint.With(expandedDocument), OriginalSelectionResult.With(expandedDocument), analyzeResult.With(expandedDocument), + ExtractLocalFunction, cancellationToken).ConfigureAwait(false); var applied = await triviaResult.ApplyAsync(generatedCode, cancellationToken).ConfigureAwait(false); diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 0c75a6e825fbfe039b97212eba12202d0193c41f..f034ded11d42e0c84d8c184dbc038031686c9299 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -1329,7 +1329,7 @@ internal class FeaturesResources { } /// - /// Looks up a localized string similar to The current content of source file '{0}' does not match the built source. The file is likely being updated by a background build. The debug session can't continue until the update is completed.. + /// Looks up a localized string similar to The current content of source file '{0}' does not match the built source. The debug session can't continue until the content of the source file is restored.. /// internal static string DocumentIsOutOfSyncWithDebuggee { get { @@ -1499,6 +1499,24 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Extract Local Method. + /// + internal static string Extract_Local_Method { + get { + return ResourceManager.GetString("Extract_Local_Method", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Extract Local Method + Local. + /// + internal static string Extract_Local_Method_plus_Local { + get { + return ResourceManager.GetString("Extract_Local_Method_plus_Local", resourceCulture); + } + } + /// /// Looks up a localized string similar to Extract Method. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index fc5ef5187e04bd82aa90e9381d67897c1485d598..85cf71c4099c93f4f8316406c99f3cf7c84a0bbe 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -598,6 +598,12 @@ Unknown + + Extract Local Method + + + Extract Local Method + Local + Extract Method diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf index 7671948c48e862ffd46be4bc4669fe7a4f441de0..f2f1454961929184d8cb4c3eb3c8268884f80f5e 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.cs.xlf @@ -217,6 +217,16 @@ Hodnota výrazu se nikdy nepoužívá. + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Nepovedlo se analyzovat tok dat pro: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf index 00186664c671f41da428276cea3c45f52734684a..c342ee24ef05826465f3700e93b2793cfee61c0b 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.de.xlf @@ -217,6 +217,16 @@ Der Ausdruckswert wird niemals verwendet. + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Fehler beim Analysieren des Datenflusses für: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf index 53d4e97cf3bdb83177727d87858aa094858586e0..25de0cdee965975bb93845d88ba6a46fe89f036c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.es.xlf @@ -217,6 +217,16 @@ El valor de la expresión no se utiliza nunca. + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} No se pudo analizar el flujo de datos para: {0}. diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf index 5d531ff13eb720f6f3d5d03b899ab194f7657c34..3af6541c0b857d8ee0ff6054ab11c970b278adcb 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.fr.xlf @@ -217,6 +217,16 @@ La valeur d'expression n'est jamais utilisée + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Impossible d’analyser le flux de données pour : {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf index 535b109b7a03305745e536c5c248f1cb8e667f63..edd4cef8cdc74a7f8002430d9503db448f06f31d 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.it.xlf @@ -217,6 +217,16 @@ Il valore dell'espressione non viene mai usato + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Non è stato possibile analizzare il flusso di dati per: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf index 99792113111dd8b6e1bd5b06f57d5e9a30fad702..1d425026d328faf004812344b33daf02f81c93f0 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ja.xlf @@ -217,6 +217,16 @@ 式の値が使用されていません + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} データ フローを分析できませんでした: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf index 5b4b9df154a99da296f0f911b09cc94b3ad59df5..307186f17fdbbbff5becbc58712d4de90cab185f 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ko.xlf @@ -217,6 +217,16 @@ 식 값은 절대 사용되지 않습니다. + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} {0}의 데이터 흐름 분석 실패 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf index 7614df533daa749df9af1cdf584b43ac28cb7d02..c5b72e20748d2ef1a3f225c131fb5a100f2d2b8a 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pl.xlf @@ -217,6 +217,16 @@ Wartość wyrażenia nie jest nigdy używana. + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Nie można przeanalizować przepływu danych dla: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf index 07418ed1685e6836a3872590926f6d620782ea30..c33e9e9d87a56a560eeabdaa1d716384bce463d9 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.pt-BR.xlf @@ -217,6 +217,16 @@ O valor da expressão nunca é usado + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Falha ao analisar o fluxo de dados para: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf index 7545a721e880a60a7f28e2acf6239ac4de3eb1e7..09b35a053e5ced9e499a179ef19ca449f25f7a5c 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.ru.xlf @@ -217,6 +217,16 @@ Значение выражения никогда не используется + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} Не удалось проанализировать поток данных для: {0} diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf index e63c7714e1c95c20eaee9d04305acc90f5637ba0..9ece6692abcf76d570ec7f35f477806fcfdac3b4 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.tr.xlf @@ -217,6 +217,16 @@ İfade değeri asla kullanılmaz + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} {0} için veri akışı analiz edilemedi diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf index f5680627af6d45dd8b684dfde669b14edcc0405d..d3f34cace7a7c52b3cdc3027bee74377d9de2045 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hans.xlf @@ -217,6 +217,16 @@ 永远不会使用表达式值 + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} 未能分析 {0} 的数据流 diff --git a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf index 5e54f1371614dd2a4aba3c286971a6821b227c55..997556b4981944975962e36ab8289ca56245c6ab 100644 --- a/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf +++ b/src/Features/Core/Portable/xlf/FeaturesResources.zh-Hant.xlf @@ -217,6 +217,16 @@ 永遠不會使用運算式值 + + Extract Local Method + Extract Local Method + + + + Extract Local Method + Local + Extract Local Method + Local + + Failed to analyze data-flow for: {0} 無法分析下列項目的資料流程: {0} diff --git a/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicExtractMethodService.vb b/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicExtractMethodService.vb index c5429052aaaa18ac30d5d6752ee27215ecb83937..18c5a711303af8914a5207d25320223ba8be149d 100644 --- a/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicExtractMethodService.vb +++ b/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicExtractMethodService.vb @@ -21,7 +21,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ExtractMethod Return New VisualBasicSelectionValidator(document, textSpan, options) End Function - Protected Overrides Function CreateMethodExtractor(selectionResult As VisualBasicSelectionResult) As VisualBasicMethodExtractor + Protected Overrides Function CreateMethodExtractor(selectionResult As VisualBasicSelectionResult, extractLocalFunction As Boolean) As VisualBasicMethodExtractor Return New VisualBasicMethodExtractor(selectionResult) End Function End Class diff --git a/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicMethodExtractor.vb b/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicMethodExtractor.vb index dd0cb3cdfbe01b71fe01cdbb465159f380d3495e..787dc2a65a6e12d46fce1a82fcb14f3c618f80ae 100644 --- a/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicMethodExtractor.vb +++ b/src/Features/VisualBasic/Portable/ExtractMethod/VisualBasicMethodExtractor.vb @@ -63,7 +63,7 @@ Namespace Microsoft.CodeAnalysis.VisualBasic.ExtractMethod Return Await selection.SemanticDocument.WithSyntaxRootAsync(selection.SemanticDocument.Root.ReplaceNode(lastExpression, newStatement), cancellationToken).ConfigureAwait(False) End Function - Protected Overrides Function GenerateCodeAsync(insertionPoint As InsertionPoint, selectionResult As SelectionResult, analyzeResult As AnalyzerResult, cancellationToken As CancellationToken) As Task(Of GeneratedCode) + Protected Overrides Function GenerateCodeAsync(insertionPoint As InsertionPoint, selectionResult As SelectionResult, analyzeResult As AnalyzerResult, extractLocalFunction As Boolean, cancellationToken As CancellationToken) As Task(Of GeneratedCode) Return VisualBasicCodeGenerator.GenerateResultAsync(insertionPoint, selectionResult, analyzeResult, cancellationToken) End Function diff --git a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationHelpers.cs b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationHelpers.cs index 9920cc747450d748c630bea937e4b3436b7ab993..7b1237c909979ab8183f80466f9d0d1f0dd2b4d3 100644 --- a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationHelpers.cs +++ b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationHelpers.cs @@ -168,6 +168,11 @@ public static MemberDeclarationSyntax LastOperator(SyntaxList m is OperatorDeclarationSyntax || m is ConversionOperatorDeclarationSyntax); } + public static StatementSyntax LastStatement(SyntaxList statements) + { + return statements.LastOrDefault(s => s is StatementSyntax); + } + public static SyntaxList Insert( SyntaxList declarationList, TDeclaration declaration, diff --git a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationService.cs b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationService.cs index c57f5aebc050faf00d2ce481ccfa6b128976a58d..26a89adb6c8e63ee57b437cf69bcf7b0c89106c9 100644 --- a/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationService.cs +++ b/src/Workspaces/CSharp/Portable/CodeGeneration/CSharpCodeGenerationService.cs @@ -111,7 +111,7 @@ protected override TDeclarationNode AddField(TDeclarationNode protected override TDeclarationNode AddMethod(TDeclarationNode destination, IMethodSymbol method, CodeGenerationOptions options, IList availableIndices) { - CheckDeclarationNode(destination); + CheckDeclarationNode(destination); // Synthesized methods for properties/events are not things we actually generate // declarations for. @@ -158,6 +158,11 @@ protected override TDeclarationNode AddMethod(TDeclarationNode typeDeclaration, method, Workspace, options, availableIndices)); } + if (destination is MethodDeclarationSyntax methodDeclaration) + { + return Cast(MethodGenerator.AddMethodTo(methodDeclaration, method, Workspace, options, availableIndices)); + } + if (method.IsConstructor() || method.IsDestructor()) { diff --git a/src/Workspaces/CSharp/Portable/CodeGeneration/MethodGenerator.cs b/src/Workspaces/CSharp/Portable/CodeGeneration/MethodGenerator.cs index b45c8c22e6a4d5e3b3bb70164e5fe0b105ba19d2..d2a1c6c161c3b3e133485ee538803af51c7b2ea5 100644 --- a/src/Workspaces/CSharp/Portable/CodeGeneration/MethodGenerator.cs +++ b/src/Workspaces/CSharp/Portable/CodeGeneration/MethodGenerator.cs @@ -5,7 +5,6 @@ using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.CSharp.CodeStyle; using Microsoft.CodeAnalysis.CSharp.Extensions; -using Microsoft.CodeAnalysis.CSharp.Symbols; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.PooledObjects; using Microsoft.CodeAnalysis.Shared.Extensions; @@ -61,6 +60,20 @@ internal static class MethodGenerator return AddMembersTo(destination, members); } + internal static MethodDeclarationSyntax AddMethodTo( + MethodDeclarationSyntax destination, + IMethodSymbol method, + Workspace workspace, + CodeGenerationOptions options, + IList availableIndices) + { + var localMethodDeclaration = GenerateLocalMethodDeclaration( + method, GetDestination(destination), workspace, options, + destination?.SyntaxTree.Options ?? options.ParseOptions); + + return destination.AddBodyStatements(localMethodDeclaration); + } + public static MethodDeclarationSyntax GenerateMethodDeclaration( IMethodSymbol method, CodeGenerationDestination destination, Workspace workspace, CodeGenerationOptions options, @@ -111,6 +124,53 @@ internal static class MethodGenerator return AddFormatterAndCodeGeneratorAnnotationsTo(methodDeclaration); } + public static LocalFunctionStatementSyntax GenerateLocalMethodDeclaration( + IMethodSymbol method, CodeGenerationDestination destination, + Workspace workspace, CodeGenerationOptions options, + ParseOptions parseOptions) + { + options ??= CodeGenerationOptions.Default; + + var reusableSyntax = GetReuseableSyntaxNodeForSymbol(method, options); + if (reusableSyntax != null) + { + return reusableSyntax; + } + + var declaration = GenerateLocalMethodDeclarationWorker( + method, destination, workspace, options, parseOptions); + + return AddAnnotationsTo(method, + ConditionallyAddDocumentationCommentTo(declaration, method, options)); + } + private static LocalFunctionStatementSyntax GenerateLocalMethodDeclarationWorker( + IMethodSymbol method, CodeGenerationDestination destination, + Workspace workspace, CodeGenerationOptions options, ParseOptions parseOptions) + { + // Don't rely on destination to decide if method body should be generated. + // Users of this service need to express their intention explicitly, either by + // setting `CodeGenerationOptions.GenerateMethodBodies` to true, or making + // `method` abstract. This would provide more flexibility. + var hasNoBody = !options.GenerateMethodBodies || method.IsAbstract; + + var explicitInterfaceSpecifier = GenerateExplicitInterfaceSpecifier(method.ExplicitInterfaceImplementations); + + var localMethodDeclaration = SyntaxFactory.LocalFunctionStatement( + modifiers: GenerateModifiers(method, destination, options), + returnType: method.GenerateReturnTypeSyntax(), + identifier: method.Name.ToIdentifierToken(), + typeParameterList: GenerateTypeParameterList(method, options), + parameterList: ParameterGenerator.GenerateParameterList(method.Parameters, explicitInterfaceSpecifier != null, options), + constraintClauses: GenerateConstraintClauses(method), + body: hasNoBody ? null : StatementGenerator.GenerateBlock(method), + expressionBody: default, + semicolonToken: hasNoBody ? SyntaxFactory.Token(SyntaxKind.SemicolonToken) : new SyntaxToken()); + + localMethodDeclaration = UseExpressionBodyIfDesired(workspace, localMethodDeclaration, parseOptions); + + return AddFormatterAndCodeGeneratorAnnotationsTo(localMethodDeclaration); + } + private static MethodDeclarationSyntax UseExpressionBodyIfDesired( Workspace workspace, MethodDeclarationSyntax methodDeclaration, ParseOptions options) { @@ -130,6 +190,25 @@ internal static class MethodGenerator return methodDeclaration; } + private static LocalFunctionStatementSyntax UseExpressionBodyIfDesired( + Workspace workspace, LocalFunctionStatementSyntax localMethodDeclaration, ParseOptions options) + { + if (localMethodDeclaration.ExpressionBody == null) + { + var expressionBodyPreference = workspace.Options.GetOption(CSharpCodeStyleOptions.PreferExpressionBodiedMethods).Value; + if (localMethodDeclaration.Body.TryConvertToArrowExpressionBody( + localMethodDeclaration.Kind(), options, expressionBodyPreference, + out var expressionBody, out var semicolonToken)) + { + return localMethodDeclaration.WithBody(null) + .WithExpressionBody(expressionBody) + .WithSemicolonToken(semicolonToken); + } + } + + return localMethodDeclaration; + } + private static SyntaxList GenerateAttributes( IMethodSymbol method, CodeGenerationOptions options, bool isExplicit) {