File: Implementation\AbstractEditorFactory.cs
Web Access
Project: src\src\VisualStudio\Core\Def\Microsoft.VisualStudio.LanguageServices_pxr0p0dn_wpftmp.csproj (Microsoft.VisualStudio.LanguageServices)
// 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.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeCleanup;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;
using Microsoft.VisualStudio.WinForms.Interfaces;
using Roslyn.Utilities;
 
namespace Microsoft.VisualStudio.LanguageServices.Implementation;
 
/// <summary>
/// The base class of both the Roslyn editor factories.
/// </summary>
internal abstract class AbstractEditorFactory : IVsEditorFactory, IVsEditorFactory4, IVsEditorFactoryNotify
{
    private readonly IComponentModel _componentModel;
    private Microsoft.VisualStudio.OLE.Interop.IServiceProvider? _oleServiceProvider;
    private bool _encoding;
 
    protected AbstractEditorFactory(IComponentModel componentModel)
        => _componentModel = componentModel;
 
    protected abstract string ContentTypeName { get; }
    protected abstract string LanguageName { get; }
 
    /// <summary>
    /// The project that is used to format newly added documents is in an unknown state - it might be
    /// fully realized, we might have only recieved part of the data about it, or it could be a temporary
    /// one that we create solely for the purpose of new document formatting. Since the language version
    /// informs what types of formatting changes might be possible, this method exists to ensure that we
    /// at least provide that piece of information regardless of anything else.
    /// </summary>
    protected abstract Project GetProjectWithCorrectParseOptionsForProject(Project project, IVsHierarchy hierarchy);
 
    public void SetEncoding(bool value)
        => _encoding = value;
 
    int IVsEditorFactory.Close()
        => VSConstants.S_OK;
 
    public int CreateEditorInstance(
        uint grfCreateDoc,
        string pszMkDocument,
        string? pszPhysicalView,
        IVsHierarchy vsHierarchy,
        uint itemid,
        IntPtr punkDocDataExisting,
        out IntPtr ppunkDocView,
        out IntPtr ppunkDocData,
        out string pbstrEditorCaption,
        out Guid pguidCmdUI,
        out int pgrfCDW)
    {
        Contract.ThrowIfNull(_oleServiceProvider);
 
        ppunkDocView = IntPtr.Zero;
        ppunkDocData = IntPtr.Zero;
        pbstrEditorCaption = string.Empty;
        pguidCmdUI = Guid.Empty;
        pgrfCDW = 0;
 
        var physicalView = pszPhysicalView ?? "Code";
        IVsTextBuffer? textBuffer = null;
 
        // Is this document already open? If so, let's see if it's a IVsTextBuffer we should re-use. This allows us
        // to properly handle multiple windows open for the same document.
        if (punkDocDataExisting != IntPtr.Zero)
        {
            var docDataExisting = Marshal.GetObjectForIUnknown(punkDocDataExisting);
 
            textBuffer = docDataExisting as IVsTextBuffer;
 
            if (textBuffer == null)
            {
                // We are incompatible with the existing doc data
                return VSConstants.VS_E_INCOMPATIBLEDOCDATA;
            }
        }
 
        var editorAdaptersFactoryService = _componentModel.GetService<IVsEditorAdaptersFactoryService>();
 
        // Do we need to create a text buffer?
        if (textBuffer == null)
        {
            textBuffer = (IVsTextBuffer)GetDocumentData(grfCreateDoc, pszMkDocument, vsHierarchy, itemid);
            Contract.ThrowIfNull(textBuffer, $"Failed to get document data for {pszMkDocument}");
        }
 
        // If the text buffer is marked as read-only, ensure that the padlock icon is displayed
        // next the new window's title and that [Read Only] is appended to title.
        var readOnlyStatus = READONLYSTATUS.ROSTATUS_NotReadOnly;
        if (ErrorHandler.Succeeded(textBuffer.GetStateFlags(out var textBufferFlags)) &&
            0 != (textBufferFlags & ((uint)BUFFERSTATEFLAGS.BSF_FILESYS_READONLY | (uint)BUFFERSTATEFLAGS.BSF_USER_READONLY)))
        {
            readOnlyStatus = READONLYSTATUS.ROSTATUS_ReadOnly;
        }
 
        switch (physicalView)
        {
            case "Form":
 
                if (CreateWinFormsEditorInstance(
                    vsHierarchy,
                    itemid,
                    textBuffer,
                    readOnlyStatus,
                    out ppunkDocView,
                    out pbstrEditorCaption,
                    out pguidCmdUI) == VSConstants.E_FAIL)
                {
                    goto case "Code";
                }
 
                break;
 
            case "Code":
 
                var codeWindow = editorAdaptersFactoryService.CreateVsCodeWindowAdapter(_oleServiceProvider);
                codeWindow.SetBuffer((IVsTextLines)textBuffer);
 
                codeWindow.GetEditorCaption(readOnlyStatus, out pbstrEditorCaption);
 
                ppunkDocView = Marshal.GetIUnknownForObject(codeWindow);
                pguidCmdUI = VSConstants.GUID_TextEditorFactory;
 
                break;
 
            default:
 
                return VSConstants.E_INVALIDARG;
        }
 
        ppunkDocData = Marshal.GetIUnknownForObject(textBuffer);
 
        return VSConstants.S_OK;
    }
 
    public object GetDocumentData(uint grfCreate, string pszMkDocument, IVsHierarchy pHier, uint itemid)
    {
        Contract.ThrowIfNull(_oleServiceProvider);
        var editorAdaptersFactoryService = _componentModel.GetService<IVsEditorAdaptersFactoryService>();
        var contentTypeRegistryService = _componentModel.GetService<IContentTypeRegistryService>();
        var contentType = contentTypeRegistryService.GetContentType(ContentTypeName);
        var textBuffer = editorAdaptersFactoryService.CreateVsTextBufferAdapter(_oleServiceProvider, contentType);
 
        if (_encoding)
        {
            if (textBuffer is IVsUserData userData)
            {
                // The editor shims require that the boxed value when setting the PromptOnLoad flag is a uint
                var hresult = userData.SetData(
                    VSConstants.VsTextBufferUserDataGuid.VsBufferEncodingPromptOnLoad_guid,
                    (uint)__PROMPTONLOADFLAGS.codepagePrompt);
 
                Marshal.ThrowExceptionForHR(hresult);
            }
        }
 
        return textBuffer;
    }
 
    public object GetDocumentView(uint grfCreate, string pszPhysicalView, IVsHierarchy pHier, IntPtr punkDocData, uint itemid)
    {
        // There is no scenario need currently to implement this method.
        throw new NotImplementedException();
    }
 
    public string GetEditorCaption(string pszMkDocument, string pszPhysicalView, IVsHierarchy pHier, IntPtr punkDocData, out Guid pguidCmdUI)
    {
        // It is not possible to get this information without initializing the designer.
        // There is no other scenario need currently to implement this method.
        throw new NotImplementedException();
    }
 
    public bool ShouldDeferUntilIntellisenseIsReady(uint grfCreate, string pszMkDocument, string pszPhysicalView)
    {
        return "Form".Equals(pszPhysicalView, StringComparison.OrdinalIgnoreCase);
    }
 
    public int MapLogicalView(ref Guid rguidLogicalView, out string? pbstrPhysicalView)
    {
        pbstrPhysicalView = null;
 
        if (rguidLogicalView == VSConstants.LOGVIEWID.Primary_guid ||
            rguidLogicalView == VSConstants.LOGVIEWID.Debugging_guid ||
            rguidLogicalView == VSConstants.LOGVIEWID.Code_guid ||
            rguidLogicalView == VSConstants.LOGVIEWID.TextView_guid)
        {
            return VSConstants.S_OK;
        }
        else if (rguidLogicalView == VSConstants.LOGVIEWID.Designer_guid)
        {
            pbstrPhysicalView = "Form";
            return VSConstants.S_OK;
        }
        else
        {
            return VSConstants.E_NOTIMPL;
        }
    }
 
    int IVsEditorFactory.SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider psp)
    {
        _oleServiceProvider = psp;
        return VSConstants.S_OK;
    }
 
    int IVsEditorFactoryNotify.NotifyDependentItemSaved(IVsHierarchy pHier, uint itemidParent, string pszMkDocumentParent, uint itemidDpendent, string pszMkDocumentDependent)
        => VSConstants.S_OK;
 
    int IVsEditorFactoryNotify.NotifyItemAdded(uint grfEFN, IVsHierarchy pHier, uint itemid, string pszMkDocument)
    {
        // Is this being added from a template?
        if (((__EFNFLAGS)grfEFN & __EFNFLAGS.EFN_ClonedFromTemplate) != 0)
        {
            var uiThreadOperationExecutor = _componentModel.GetService<IUIThreadOperationExecutor>();
            // TODO(cyrusn): Can this be cancellable?
            uiThreadOperationExecutor.Execute(
                "Intellisense",
                defaultDescription: "",
                allowCancellation: false,
                showProgress: false,
                action: c => FormatDocumentCreatedFromTemplate(pHier, pszMkDocument, c.UserCancellationToken));
        }
 
        return VSConstants.S_OK;
    }
 
    int IVsEditorFactoryNotify.NotifyItemRenamed(IVsHierarchy pHier, uint itemid, string pszMkDocumentOld, string pszMkDocumentNew)
        => VSConstants.S_OK;
 
    private void FormatDocumentCreatedFromTemplate(IVsHierarchy hierarchy, string filePath, CancellationToken cancellationToken)
    {
        var threadingContext = _componentModel.GetService<IThreadingContext>();
        threadingContext.JoinableTaskFactory.Run(() => FormatDocumentCreatedFromTemplateAsync(hierarchy, filePath, cancellationToken));
    }
 
    // NOTE: This function has been created to hide IWinFormsEditorFactory type in non-WinForms scenarios (e.g. editing .cs or .vb file)
    // so that its corresponding dll doesn't get loaded. Due to this reason, function inlining has been disabled.
    [MethodImpl(MethodImplOptions.NoInlining)]
    private int CreateWinFormsEditorInstance(
        IVsHierarchy vsHierarchy,
        uint itemid,
        IVsTextBuffer textBuffer,
        READONLYSTATUS readOnlyStatus,
        out IntPtr ppunkDocView,
        out string pbstrEditorCaption,
        out Guid pguidCmdUI)
    {
        ppunkDocView = IntPtr.Zero;
        pbstrEditorCaption = string.Empty;
        pguidCmdUI = Guid.Empty;
 
        var winFormsEditorFactory = (IWinFormsEditorFactory)PackageUtilities.QueryService<IWinFormsEditorFactory>(_oleServiceProvider);
 
        return winFormsEditorFactory is null
            ? VSConstants.E_FAIL
            : winFormsEditorFactory.CreateEditorInstance(
                vsHierarchy,
                itemid,
                _oleServiceProvider,
                textBuffer,
                readOnlyStatus,
                out ppunkDocView,
                out pbstrEditorCaption,
                out pguidCmdUI);
    }
 
    private async Task FormatDocumentCreatedFromTemplateAsync(IVsHierarchy hierarchy, string filePath, CancellationToken cancellationToken)
    {
        // A file has been created on disk which the user added from the "Add Item" dialog. We need
        // to include this in a workspace to figure out the right options it should be formatted with.
        // This requires us to place it in the correct project.
        var workspace = _componentModel.GetService<VisualStudioWorkspace>();
        var solution = workspace.CurrentSolution;
 
        Project? projectToAddTo = null;
 
        foreach (var projectId in solution.ProjectIds)
        {
            if (workspace.GetHierarchy(projectId) == hierarchy)
            {
                projectToAddTo = solution.GetRequiredProject(projectId);
                break;
            }
        }
 
        if (projectToAddTo == null)
        {
            // We don't have a project for this, so we'll just make up a fake project altogether
            projectToAddTo = solution.AddProject(
                name: nameof(FormatDocumentCreatedFromTemplate),
                assemblyName: nameof(FormatDocumentCreatedFromTemplate),
                language: LanguageName);
 
            // We have to discover .editorconfig files ourselves to ensure that code style rules are followed.
            // Normally the project system would tell us about these.
            projectToAddTo = AddEditorConfigFiles(projectToAddTo, Path.GetDirectoryName(filePath));
        }
 
        // We need to ensure that decisions made during new document formatting are based on the right language
        // version from the project system, but the NotifyItemAdded event happens before a design time build,
        // and sometimes before we have even been told about the projects existence, so we have to ask the hierarchy
        // for the language version to use.
        projectToAddTo = GetProjectWithCorrectParseOptionsForProject(projectToAddTo, hierarchy);
 
        var documentId = DocumentId.CreateNewId(projectToAddTo.Id);
 
        var fileLoader = new WorkspaceFileTextLoader(solution.Services, filePath, defaultEncoding: null);
        var forkedSolution = projectToAddTo.Solution.AddDocument(
            DocumentInfo.Create(
                documentId,
                name: filePath,
                loader: fileLoader,
                filePath: filePath));
 
        var addedDocument = forkedSolution.GetRequiredDocument(documentId);
 
        var cleanupOptions = await addedDocument.GetCodeCleanupOptionsAsync(cancellationToken).ConfigureAwait(true);
 
        // Call out to various new document formatters to tweak what they want
        var formattingService = addedDocument.GetLanguageService<INewDocumentFormattingService>();
        if (formattingService is not null)
        {
            addedDocument = await formattingService.FormatNewDocumentAsync(addedDocument, hintDocument: null, cleanupOptions, cancellationToken).ConfigureAwait(true);
        }
 
        var rootToFormat = await addedDocument.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(true);
 
        // Format document
        var unformattedText = await addedDocument.GetValueTextAsync(cancellationToken).ConfigureAwait(true);
        var formattedRoot = Formatter.Format(rootToFormat, workspace.Services.SolutionServices, cleanupOptions.FormattingOptions, cancellationToken);
        var formattedText = formattedRoot.GetText(unformattedText.Encoding, unformattedText.ChecksumAlgorithm);
 
        // Ensure the line endings are normalized. The formatter doesn't touch everything if it doesn't need to.
        var targetLineEnding = cleanupOptions.FormattingOptions.NewLine;
 
        var originalText = formattedText;
        foreach (var originalLine in originalText.Lines)
        {
            var originalNewLine = originalText.ToString(CodeAnalysis.Text.TextSpan.FromBounds(originalLine.End, originalLine.EndIncludingLineBreak));
 
            // Check if we have a line ending, so we don't go adding one to the end if we don't need to.
            if (originalNewLine.Length > 0 && originalNewLine != targetLineEnding)
            {
                var currentLine = formattedText.Lines[originalLine.LineNumber];
                var currentSpan = CodeAnalysis.Text.TextSpan.FromBounds(currentLine.End, currentLine.EndIncludingLineBreak);
                formattedText = formattedText.WithChanges(new TextChange(currentSpan, targetLineEnding));
            }
        }
 
        IOUtilities.PerformIO(() =>
        {
            using var textWriter = new StreamWriter(filePath, append: false, encoding: formattedText.Encoding);
            // We pass null here for cancellation, since cancelling in the middle of the file write would leave the file corrupted
            formattedText.Write(textWriter, cancellationToken: CancellationToken.None);
        });
    }
 
    private static Project AddEditorConfigFiles(Project projectToAddTo, string projectFolder)
    {
        do
        {
            projectToAddTo = AddEditorConfigFile(projectToAddTo, projectFolder, out var foundRoot);
 
            if (foundRoot)
                break;
 
            projectFolder = Path.GetDirectoryName(projectFolder);
        }
        while (projectFolder is not null);
 
        return projectToAddTo;
 
        static Project AddEditorConfigFile(Project project, string folder, out bool foundRoot)
        {
            const string EditorConfigFileName = ".editorconfig";
 
            foundRoot = false;
 
            var editorConfigFile = Path.Combine(folder, EditorConfigFileName);
 
            var text = IOUtilities.PerformIO(() =>
            {
                using var stream = File.OpenRead(editorConfigFile);
                return SourceText.From(stream);
            });
 
            if (text is null)
                return project;
 
            return project.AddAnalyzerConfigDocument(EditorConfigFileName, text, filePath: editorConfigFile).Project;
        }
    }
}