-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Add feature to convert to file-scoped namespace just by typing ; after a normal namespace.
#58003
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
Changes from all commits
ce69a5c
d2b7692
5e8d11c
2eebe11
856f804
cee1445
c6d6da4
b336159
4d8f8ab
c4b2c21
eb6934a
81ed168
bff627e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
| // See the LICENSE file in the project root for more information. | ||
|
|
||
| using System; | ||
| using System.Collections.Immutable; | ||
| using System.ComponentModel.Composition; | ||
| using System.Linq; | ||
| using Microsoft.CodeAnalysis.CodeStyle; | ||
| using Microsoft.CodeAnalysis.CSharp; | ||
| using Microsoft.CodeAnalysis.CSharp.CodeStyle; | ||
| using Microsoft.CodeAnalysis.CSharp.ConvertNamespace; | ||
| using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
| using Microsoft.CodeAnalysis.Editor.Implementation.AutomaticCompletion; | ||
| using Microsoft.CodeAnalysis.Editor.Shared.Extensions; | ||
| using Microsoft.CodeAnalysis.Editor.Shared.Options; | ||
| using Microsoft.CodeAnalysis.Editor.Shared.Utilities; | ||
| using Microsoft.CodeAnalysis.Formatting; | ||
| using Microsoft.CodeAnalysis.Host.Mef; | ||
| using Microsoft.CodeAnalysis.Options; | ||
| using Microsoft.CodeAnalysis.Shared.Extensions; | ||
| using Microsoft.CodeAnalysis.Text; | ||
| using Microsoft.VisualStudio.Commanding; | ||
| using Microsoft.VisualStudio.Language.Intellisense.AsyncCompletion; | ||
| using Microsoft.VisualStudio.Text; | ||
| using Microsoft.VisualStudio.Text.Editor.Commanding.Commands; | ||
| using Microsoft.VisualStudio.Text.Operations; | ||
| using Microsoft.VisualStudio.Utilities; | ||
|
|
||
| namespace Microsoft.CodeAnalysis.Editor.CSharp.CompleteStatement | ||
| { | ||
| /// <summary> | ||
| /// Converts a block-scoped namespace to a file-scoped one if the user types <c>;</c> after its name. | ||
| /// </summary> | ||
| [Export(typeof(ICommandHandler))] | ||
| [Export] | ||
| [ContentType(ContentTypeNames.CSharpContentType)] | ||
| [Name(nameof(ConvertNamespaceCommandHandler))] | ||
| [Order(After = PredefinedCompletionNames.CompletionCommandHandler)] | ||
| internal sealed class ConvertNamespaceCommandHandler : IChainedCommandHandler<TypeCharCommandArgs> | ||
| { | ||
| /// <summary> | ||
| /// Annotation used so we can find the semicolon after formatting so that we can properly place the caret. | ||
| /// </summary> | ||
| private static readonly SyntaxAnnotation s_annotation = new(); | ||
|
|
||
| /// <summary> | ||
| /// A fake option set where the 'use file scoped' namespace option is on. That way we can call into the helpers | ||
| /// and have the results come back positive for converting to file-scoped regardless of the current option | ||
| /// value. | ||
| /// </summary> | ||
| private static readonly OptionSet s_optionSet = new OptionValueSet( | ||
| ImmutableDictionary<OptionKey, object?>.Empty.Add( | ||
| new OptionKey(CSharpCodeStyleOptions.NamespaceDeclarations.ToPublicOption()), | ||
| new CodeStyleOption2<NamespaceDeclarationPreference>( | ||
| NamespaceDeclarationPreference.FileScoped, | ||
| NotificationOption2.Suggestion))); | ||
|
|
||
| private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry; | ||
| private readonly IEditorOperationsFactoryService _editorOperationsFactoryService; | ||
| private readonly IGlobalOptionService _globalOptions; | ||
|
|
||
| [ImportingConstructor] | ||
| [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
| public ConvertNamespaceCommandHandler( | ||
| ITextUndoHistoryRegistry textUndoHistoryRegistry, | ||
| IEditorOperationsFactoryService editorOperationsFactoryService, IGlobalOptionService globalOptions) | ||
| { | ||
| _textUndoHistoryRegistry = textUndoHistoryRegistry; | ||
| _editorOperationsFactoryService = editorOperationsFactoryService; | ||
| _globalOptions = globalOptions; | ||
| } | ||
|
|
||
| public CommandState GetCommandState(TypeCharCommandArgs args, Func<CommandState> nextCommandHandler) | ||
| => nextCommandHandler(); | ||
|
|
||
| public string DisplayName => CSharpAnalyzersResources.Convert_to_file_scoped_namespace; | ||
|
|
||
| public void ExecuteCommand(TypeCharCommandArgs args, Action nextCommandHandler, CommandExecutionContext executionContext) | ||
| { | ||
| // Attempt to convert the block-namespace to a file-scoped namespace if we're at the right location. | ||
| var convertedRoot = ConvertNamespace(args, executionContext); | ||
|
|
||
| // No matter if we succeeded or not, insert the semicolon. This way, when we convert, the user can still | ||
| // hit ctrl-z to get back to the code with just the semicolon inserted. | ||
| nextCommandHandler(); | ||
|
|
||
| // If we weren't on a block namespace (or couldn't convert it for some reason), then bail out after | ||
| // inserting the semicolon. | ||
| if (convertedRoot == null) | ||
| return; | ||
|
|
||
| // Otherwise, make a transaction for the edit and replace the buffer with the final text. | ||
| using var transaction = CaretPreservingEditTransaction.TryCreate( | ||
| this.DisplayName, args.TextView, _textUndoHistoryRegistry, _editorOperationsFactoryService); | ||
|
|
||
| var edit = args.SubjectBuffer.CreateEdit(EditOptions.DefaultMinimalChange, reiteratedVersionNumber: null, editTag: null); | ||
| edit.Replace(new Span(0, args.SubjectBuffer.CurrentSnapshot.Length), convertedRoot.ToFullString()); | ||
|
|
||
| edit.Apply(); | ||
|
|
||
| // Attempt to place the caret right after the semicolon of the file-scoped namespace. | ||
| var annotatedToken = convertedRoot.GetAnnotatedTokens(s_annotation).FirstOrDefault(); | ||
| if (annotatedToken != default) | ||
| args.TextView.Caret.MoveTo(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, annotatedToken.Span.End)); | ||
|
|
||
| transaction?.Complete(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Returns the updated file contents if semicolon is typed after a block-scoped namespace name that can be | ||
| /// converted. | ||
| /// </summary> | ||
| private CompilationUnitSyntax? ConvertNamespace( | ||
| TypeCharCommandArgs args, | ||
| CommandExecutionContext executionContext) | ||
| { | ||
| if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty) | ||
| return null; | ||
|
|
||
| if (!_globalOptions.GetOption(FeatureOnOffOptions.AutomaticallyCompleteStatementOnSemicolon)) | ||
| return null; | ||
|
|
||
| var subjectBuffer = args.SubjectBuffer; | ||
| var caretOpt = args.TextView.GetCaretPoint(subjectBuffer); | ||
| if (!caretOpt.HasValue) | ||
| return null; | ||
|
|
||
| var caret = caretOpt.Value.Position; | ||
| var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges(); | ||
| if (document == null) | ||
| return null; | ||
|
|
||
| var cancellationToken = executionContext.OperationContext.UserCancellationToken; | ||
| var root = (CompilationUnitSyntax)document.GetRequiredSyntaxRootSynchronously(cancellationToken); | ||
|
|
||
| // User has to be *after* an identifier token. | ||
| var token = root.FindToken(caret); | ||
| if (token.Kind() != SyntaxKind.IdentifierToken) | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return null; | ||
|
|
||
| if (caret < token.Span.End || | ||
| caret >= token.FullSpan.End) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var namespaceDecl = token.GetRequiredParent().GetAncestor<NamespaceDeclarationSyntax>(); | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (namespaceDecl == null) | ||
| return null; | ||
|
|
||
| // That identifier token has to be the last part of a namespace name. | ||
| if (namespaceDecl.Name.GetLastToken() != token) | ||
| return null; | ||
|
|
||
| // Pass in our special options, and C#10 so that if we can convert this to file-scoped, we will. | ||
| if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_optionSet, root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10)) | ||
CyrusNajmabadi marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return null; | ||
|
|
||
| var fileScopedNamespace = (FileScopedNamespaceDeclarationSyntax)ConvertNamespaceTransform.Convert(namespaceDecl); | ||
|
|
||
| // Place an annotation on the semicolon so that we can find it post-formatting to place the caret. | ||
| fileScopedNamespace = fileScopedNamespace.WithSemicolonToken( | ||
| fileScopedNamespace.SemicolonToken.WithAdditionalAnnotations(s_annotation)); | ||
|
|
||
| var convertedRoot = root.ReplaceNode(namespaceDecl, fileScopedNamespace); | ||
| var formattedRoot = (CompilationUnitSyntax)Formatter.Format( | ||
| convertedRoot, Formatter.Annotation, | ||
| document.Project.Solution.Workspace, | ||
| options: null, rules: null, cancellationToken); | ||
|
Comment on lines
+167
to
+170
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❗ This assumes the file is formatted according to the formatter, which is not a safe assumption. This feature is only allowed to remove a uniform number of leading spaces from each line in the namespace block, and is not allowed to force potentially incorrect formatting throughout the file.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I saw this tweet that indicated the codefix for converting to file scoped namespaces forced a format on users https://twitter.com/rickbrewPDN/status/1463361397677379590
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yah, this has come up on Discord a couple of times too, and ASP.NET ran into it too: dotnet/aspnetcore#38681 (comment) |
||
|
|
||
| return formattedRoot; | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.