|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | +// See the LICENSE file in the project root for more information. |
| 4 | + |
| 5 | +using System; |
| 6 | +using System.Collections.Immutable; |
| 7 | +using System.ComponentModel.Composition; |
| 8 | +using System.Linq; |
| 9 | +using Microsoft.CodeAnalysis.CodeStyle; |
| 10 | +using Microsoft.CodeAnalysis.CSharp; |
| 11 | +using Microsoft.CodeAnalysis.CSharp.CodeStyle; |
| 12 | +using Microsoft.CodeAnalysis.CSharp.ConvertNamespace; |
| 13 | +using Microsoft.CodeAnalysis.CSharp.Syntax; |
| 14 | +using Microsoft.CodeAnalysis.Editor.Implementation.AutomaticCompletion; |
| 15 | +using Microsoft.CodeAnalysis.Editor.Shared.Extensions; |
| 16 | +using Microsoft.CodeAnalysis.Editor.Shared.Options; |
| 17 | +using Microsoft.CodeAnalysis.Editor.Shared.Utilities; |
| 18 | +using Microsoft.CodeAnalysis.Formatting; |
| 19 | +using Microsoft.CodeAnalysis.Host.Mef; |
| 20 | +using Microsoft.CodeAnalysis.Options; |
| 21 | +using Microsoft.CodeAnalysis.Shared.Extensions; |
| 22 | +using Microsoft.CodeAnalysis.Text; |
| 23 | +using Microsoft.VisualStudio.Commanding; |
| 24 | +using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; |
| 25 | +using Microsoft.VisualStudio.Text; |
| 26 | +using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; |
| 27 | +using Microsoft.VisualStudio.Text.Operations; |
| 28 | +using Microsoft.VisualStudio.Utilities; |
| 29 | + |
| 30 | +namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement |
| 31 | +{ |
| 32 | + /// <summary> |
| 33 | + /// Converts a block-scoped namespace to a file-scoped one if the user types <c>;</c> after its name. |
| 34 | + /// </summary> |
| 35 | + [Export(typeof(ICommandHandler))] |
| 36 | + [Export] |
| 37 | + [ContentType(ContentTypeNames.CSharpContentType)] |
| 38 | + [Name(nameof(ConvertNamespaceCommandHandler))] |
| 39 | + [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] |
| 40 | + internal sealed class ConvertNamespaceCommandHandler : IChainedCommandHandler<TypeCharCommandArgs> |
| 41 | + { |
| 42 | + /// <summary> |
| 43 | + /// Annotation used so we can find the semicolon after formatting so that we can properly place the caret. |
| 44 | + /// </summary> |
| 45 | + private static readonly SyntaxAnnotation s_annotation = new(); |
| 46 | + |
| 47 | + /// <summary> |
| 48 | + /// A fake option set where the 'use file scoped' namespace option is on. That way we can call into the helpers |
| 49 | + /// and have the results come back positive for converting to file-scoped regardless of the current option |
| 50 | + /// value. |
| 51 | + /// </summary> |
| 52 | + private static readonly OptionSet s_optionSet = new OptionValueSet( |
| 53 | + ImmutableDictionary<OptionKey, object?>.Empty.Add( |
| 54 | + new OptionKey(CSharpCodeStyleOptions.NamespaceDeclarations.ToPublicOption()), |
| 55 | + new CodeStyleOption2<NamespaceDeclarationPreference>( |
| 56 | + NamespaceDeclarationPreference.FileScoped, |
| 57 | + NotificationOption2.Suggestion))); |
| 58 | + |
| 59 | + private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry; |
| 60 | + private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; |
| 61 | + private readonly IGlobalOptionService _globalOptions; |
| 62 | + |
| 63 | + [ImportingConstructor] |
| 64 | + [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] |
| 65 | + public ConvertNamespaceCommandHandler( |
| 66 | + ITextUndoHistoryRegistry textUndoHistoryRegistry, |
| 67 | + IEditorOperationsFactoryService editorOperationsFactoryService, IGlobalOptionService globalOptions) |
| 68 | + { |
| 69 | + _textUndoHistoryRegistry = textUndoHistoryRegistry; |
| 70 | + _editorOperationsFactoryService = editorOperationsFactoryService; |
| 71 | + _globalOptions = globalOptions; |
| 72 | + } |
| 73 | + |
| 74 | + public CommandState GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) |
| 75 | + => nextCommandHandler(); |
| 76 | + |
| 77 | + public string DisplayName => CSharpAnalyzersResources.Convert_to_file_scoped_namespace; |
| 78 | + |
| 79 | + public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) |
| 80 | + { |
| 81 | + // Attempt to convert the block-namespace to a file-scoped namespace if we're at the right location. |
| 82 | + var convertedRoot = ConvertNamespace(args, executionContext); |
| 83 | + |
| 84 | + // No matter if we succeeded or not, insert the semicolon. This way, when we convert, the user can still |
| 85 | + // hit ctrl-z to get back to the code with just the semicolon inserted. |
| 86 | + nextCommandHandler(); |
| 87 | + |
| 88 | + // If we weren't on a block namespace (or couldn't convert it for some reason), then bail out after |
| 89 | + // inserting the semicolon. |
| 90 | + if (convertedRoot == null) |
| 91 | + return; |
| 92 | + |
| 93 | + // Otherwise, make a transaction for the edit and replace the buffer with the final text. |
| 94 | + using var transaction = CaretPreservingEditTransaction.TryCreate( |
| 95 | + this.DisplayName, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); |
| 96 | + |
| 97 | + var edit = args.SubjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); |
| 98 | + edit.Replace(new Span(0, args.SubjectBuffer.CurrentSnapshot.Length), convertedRoot.ToFullString()); |
| 99 | + |
| 100 | + edit.Apply(); |
| 101 | + |
| 102 | + // Attempt to place the caret right after the semicolon of the file-scoped namespace. |
| 103 | + var annotatedToken = convertedRoot.GetAnnotatedTokens(s_annotation).FirstOrDefault(); |
| 104 | + if (annotatedToken != default) |
| 105 | + args.TextView.Caret.MoveTo(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, annotatedToken.Span.End)); |
| 106 | + |
| 107 | + transaction?.Complete(); |
| 108 | + } |
| 109 | + |
| 110 | + /// <summary> |
| 111 | + /// Returns the updated file contents if semicolon is typed after a block-scoped namespace name that can be |
| 112 | + /// converted. |
| 113 | + /// </summary> |
| 114 | + private CompilationUnitSyntax? ConvertNamespace( |
| 115 | + TypeCharCommandArgs args, |
| 116 | + CommandExecutionContext executionContext) |
| 117 | + { |
| 118 | + if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) |
| 119 | + return null; |
| 120 | + |
| 121 | + if (!_globalOptions.GetOption(FeatureOnOffOptions.AutomaticallyCompleteStatementOnSemicolon)) |
| 122 | + return null; |
| 123 | + |
| 124 | + var subjectBuffer = args.SubjectBuffer; |
| 125 | + var caretOpt = args.TextView.GetCaretPoint(subjectBuffer); |
| 126 | + if (!caretOpt.HasValue) |
| 127 | + return null; |
| 128 | + |
| 129 | + var caret = caretOpt.Value.Position; |
| 130 | + var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); |
| 131 | + if (document == null) |
| 132 | + return null; |
| 133 | + |
| 134 | + var cancellationToken = executionContext.OperationContext.UserCancellationToken; |
| 135 | + var root = (CompilationUnitSyntax)document.GetRequiredSyntaxRootSynchronously(cancellationToken); |
| 136 | + |
| 137 | + // User has to be *after* an identifier token. |
| 138 | + var token = root.FindToken(caret); |
| 139 | + if (token.Kind() != SyntaxKind.IdentifierToken) |
| 140 | + return null; |
| 141 | + |
| 142 | + if (caret < token.Span.End || |
| 143 | + caret >= token.FullSpan.End) |
| 144 | + { |
| 145 | + return null; |
| 146 | + } |
| 147 | + |
| 148 | + var namespaceDecl = token.GetRequiredParent().GetAncestor<NamespaceDeclarationSyntax>(); |
| 149 | + if (namespaceDecl == null) |
| 150 | + return null; |
| 151 | + |
| 152 | + // That identifier token has to be the last part of a namespace name. |
| 153 | + if (namespaceDecl.Name.GetLastToken() != token) |
| 154 | + return null; |
| 155 | + |
| 156 | + // Pass in our special options, and C#10 so that if we can convert this to file-scoped, we will. |
| 157 | + if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_optionSet, root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10)) |
| 158 | + return null; |
| 159 | + |
| 160 | + var fileScopedNamespace = (FileScopedNamespaceDeclarationSyntax)ConvertNamespaceTransform.Convert(namespaceDecl); |
| 161 | + |
| 162 | + // Place an annotation on the semicolon so that we can find it post-formatting to place the caret. |
| 163 | + fileScopedNamespace = fileScopedNamespace.WithSemicolonToken( |
| 164 | + fileScopedNamespace.SemicolonToken.WithAdditionalAnnotations(s_annotation)); |
| 165 | + |
| 166 | + var convertedRoot = root.ReplaceNode(namespaceDecl, fileScopedNamespace); |
| 167 | + var formattedRoot = (CompilationUnitSyntax)Formatter.Format( |
| 168 | + convertedRoot, Formatter.Annotation, |
| 169 | + document.Project.Solution.Workspace, |
| 170 | + options: null, rules: null, cancellationToken); |
| 171 | + |
| 172 | + return formattedRoot; |
| 173 | + } |
| 174 | + } |
| 175 | +} |
0 commit comments