File: ConvertNamespace\ConvertNamespaceCommandHandler.cs
Web Access
Project: src\src\EditorFeatures\CSharp\Microsoft.CodeAnalysis.CSharp.EditorFeatures.csproj (Microsoft.CodeAnalysis.CSharp.EditorFeatures)
// 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.ComponentModel.Composition;
using Microsoft.CodeAnalysis.AutomaticCompletion;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.ConvertNamespace;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
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;
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)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal sealed class ConvertNamespaceCommandHandler(
    ITextUndoHistoryRegistry textUndoHistoryRegistry,
    IEditorOperationsFactoryService editorOperationsFactoryService,
    EditorOptionsService editorOptionsService,
    IGlobalOptionService globalOptions,
    IIndentationManagerService indentationManager) : IChainedCommandHandler<TypeCharCommandArgs>
{
    /// <summary>
    /// Option setting 'use file scoped'.  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 CodeStyleOption2<NamespaceDeclarationPreference> s_fileScopedNamespacePreferenceOption =
        new(NamespaceDeclarationPreference.FileScoped, NotificationOption2.Suggestion);
 
    private readonly ITextUndoHistoryRegistry _textUndoHistoryRegistry = textUndoHistoryRegistry;
    private readonly IEditorOperationsFactoryService _editorOperationsFactoryService = editorOperationsFactoryService;
    private readonly EditorOptionsService _editorOptionsService = editorOptionsService;
    private readonly IIndentationManagerService _indentationManager = indentationManager;
    private readonly IGlobalOptionService _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 (convertedText, semicolonSpan) = 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 (convertedText == 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), convertedText.ToString());
 
        edit.Apply();
 
        // Place the caret right after the semicolon of the file-scoped namespace.
        args.TextView.Caret.MoveTo(new SnapshotPoint(args.SubjectBuffer.CurrentSnapshot, semicolonSpan.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 (SourceText? convertedText, TextSpan semicolonSpan) ConvertNamespace(
        TypeCharCommandArgs args,
        CommandExecutionContext executionContext)
    {
        if (args.TypedChar != ';' || !args.TextView.Selection.IsEmpty)
            return default;
 
        if (!_globalOptions.GetOption(CompleteStatementOptionsStorage.AutomaticallyCompleteStatementOnSemicolon))
            return default;
 
        var subjectBuffer = args.SubjectBuffer;
        var caretOpt = args.TextView.GetCaretPoint(subjectBuffer);
        if (!caretOpt.HasValue)
            return default;
 
        var caret = caretOpt.Value.Position;
        var document = subjectBuffer.CurrentSnapshot.GetOpenDocumentInCurrentContextWithChanges();
        if (document == null)
            return default;
 
        var cancellationToken = executionContext.OperationContext.UserCancellationToken;
        var parsedDocument = ParsedDocument.CreateSynchronously(document, cancellationToken);
 
        // User has to be *after* an identifier token.
        var token = parsedDocument.Root.FindToken(caret);
        if (token.Kind() != SyntaxKind.IdentifierToken)
            return default;
 
        if (caret < token.Span.End ||
            caret >= token.FullSpan.End)
        {
            return default;
        }
 
        var namespaceDecl = token.GetRequiredParent().GetAncestor<NamespaceDeclarationSyntax>();
        if (namespaceDecl == null)
            return default;
 
        // That identifier token has to be the last part of a namespace name.
        if (namespaceDecl.Name.GetLastToken() != token)
            return default;
 
        // Pass in our special options, and C#10 so that if we can convert this to file-scoped, we will.
        if (!ConvertNamespaceAnalysis.CanOfferUseFileScoped(s_fileScopedNamespacePreferenceOption, (CompilationUnitSyntax)parsedDocument.Root, namespaceDecl, forAnalyzer: true, LanguageVersion.CSharp10))
            return default;
 
        var formattingOptions = subjectBuffer.GetSyntaxFormattingOptions(_editorOptionsService, document.Project.GetFallbackAnalyzerOptions(), document.Project.Services, explicitFormat: false);
        return ConvertNamespaceTransform.ConvertNamespaceDeclaration(parsedDocument, namespaceDecl, formattingOptions, cancellationToken);
    }
}