AbstractEditorFactory.cs 14.3 KB
Newer Older
1 2
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

3 4
#nullable enable

5
using System;
6
using System.IO;
7
using System.Runtime.InteropServices;
D
Dustin Campbell 已提交
8
using System.Runtime.Versioning;
9
using System.Threading;
10
using Microsoft.CodeAnalysis;
11 12
using Microsoft.CodeAnalysis.Editor.Host;
using Microsoft.CodeAnalysis.Editor.Shared.Extensions;
13 14
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Utilities;
15 16 17 18
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Designer.Interfaces;
using Microsoft.VisualStudio.Editor;
19
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
20 21 22 23 24 25 26 27 28 29
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Utilities;

namespace Microsoft.VisualStudio.LanguageServices.Implementation
{
    /// <summary>
    /// The base class of both the Roslyn editor factories.
    /// </summary>
30
    internal abstract class AbstractEditorFactory : IVsEditorFactory, IVsEditorFactoryNotify
31 32
    {
        private readonly IComponentModel _componentModel;
33
        private Microsoft.VisualStudio.OLE.Interop.IServiceProvider? _oleServiceProvider;
34 35
        private bool _encoding;

36
        protected AbstractEditorFactory(IComponentModel componentModel)
37
        {
38
            _componentModel = componentModel;
39 40 41
        }

        protected abstract string ContentTypeName { get; }
42
        protected abstract string LanguageName { get; }
43 44 45 46 47 48 49 50 51 52 53 54 55 56

        public void SetEncoding(bool value)
        {
            _encoding = value;
        }

        int IVsEditorFactory.Close()
        {
            return VSConstants.S_OK;
        }

        public int CreateEditorInstance(
            uint grfCreateDoc,
            string pszMkDocument,
57
            string? pszPhysicalView,
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
            IVsHierarchy vsHierarchy,
            uint itemid,
            IntPtr punkDocDataExisting,
            out IntPtr ppunkDocView,
            out IntPtr ppunkDocData,
            out string pbstrEditorCaption,
            out Guid pguidCmdUI,
            out int pgrfCDW)
        {
            ppunkDocView = IntPtr.Zero;
            ppunkDocData = IntPtr.Zero;
            pbstrEditorCaption = string.Empty;
            pguidCmdUI = Guid.Empty;
            pgrfCDW = 0;

C
Cyrus Najmabadi 已提交
73
            var physicalView = pszPhysicalView ?? "Code";
74
            IVsTextBuffer? textBuffer = null;
75 76 77 78 79

            // 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)
            {
80
                var docDataExisting = Marshal.GetObjectForIUnknown(punkDocDataExisting);
81 82 83 84 85 86 87 88 89 90

                textBuffer = docDataExisting as IVsTextBuffer;

                if (textBuffer == null)
                {
                    // We are incompatible with the existing doc data
                    return VSConstants.VS_E_INCOMPATIBLEDOCDATA;
                }
            }

91 92
            var editorAdaptersFactoryService = _componentModel.GetService<IVsEditorAdaptersFactoryService>();

93 94 95
            // Do we need to create a text buffer?
            if (textBuffer == null)
            {
96 97 98
                var contentTypeRegistryService = _componentModel.GetService<IContentTypeRegistryService>();
                var contentType = contentTypeRegistryService.GetContentType(ContentTypeName);
                textBuffer = editorAdaptersFactoryService.CreateVsTextBufferAdapter(_oleServiceProvider, contentType);
99 100 101

                if (_encoding)
                {
C
CyrusNajmabadi 已提交
102
                    if (textBuffer is IVsUserData userData)
103 104
                    {
                        // The editor shims require that the boxed value when setting the PromptOnLoad flag is a uint
105
                        var hresult = userData.SetData(
106 107 108 109 110 111 112 113 114 115 116 117 118
                            VSConstants.VsTextBufferUserDataGuid.VsBufferEncodingPromptOnLoad_guid,
                            (uint)__PROMPTONLOADFLAGS.codepagePrompt);

                        if (ErrorHandler.Failed(hresult))
                        {
                            return hresult;
                        }
                    }
                }
            }

            // 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.
119
            var readOnlyStatus = READONLYSTATUS.ROSTATUS_NotReadOnly;
C
CyrusNajmabadi 已提交
120
            if (ErrorHandler.Succeeded(textBuffer.GetStateFlags(out var textBufferFlags)) &&
121 122 123 124 125 126 127 128 129 130
                0 != (textBufferFlags & ((uint)BUFFERSTATEFLAGS.BSF_FILESYS_READONLY | (uint)BUFFERSTATEFLAGS.BSF_USER_READONLY)))
            {
                readOnlyStatus = READONLYSTATUS.ROSTATUS_ReadOnly;
            }

            switch (physicalView)
            {
                case "Form":

                    // We must create the WinForms designer here
D
Dustin Campbell 已提交
131
                    var loaderName = GetWinFormsLoaderName(vsHierarchy);
132 133 134 135
                    if (loaderName is null)
                    {
                        goto case "Code";
                    }
136

137
                    var designerService = (IVSMDDesignerService)_oleServiceProvider.QueryService<SVSMDDesignerService>();
138
                    var designerLoader = (IVSMDDesignerLoader)designerService.CreateDesignerLoader(loaderName);
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158

                    try
                    {
                        designerLoader.Initialize(_oleServiceProvider, vsHierarchy, (int)itemid, (IVsTextLines)textBuffer);
                        pbstrEditorCaption = designerLoader.GetEditorCaption((int)readOnlyStatus);

                        var designer = designerService.CreateDesigner(_oleServiceProvider, designerLoader);
                        ppunkDocView = Marshal.GetIUnknownForObject(designer.View);
                        pguidCmdUI = designer.CommandGuid;
                    }
                    catch
                    {
                        designerLoader.Dispose();
                        throw;
                    }

                    break;

                case "Code":

159
                    var codeWindow = editorAdaptersFactoryService.CreateVsCodeWindowAdapter(_oleServiceProvider);
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
                    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;
        }

179
        private string? GetWinFormsLoaderName(IVsHierarchy vsHierarchy)
D
Dustin Campbell 已提交
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
        {
            const string LoaderName = "Microsoft.VisualStudio.Design.Serialization.CodeDom.VSCodeDomDesignerLoader";
            const string NewLoaderName = "Microsoft.VisualStudio.Design.Core.Serialization.CodeDom.VSCodeDomDesignerLoader";

            // If this is a netcoreapp3.0 (or newer), we must create the newer WinForms designer.
            // TODO: This check will eventually move into the WinForms designer itself.
            if (!vsHierarchy.TryGetTargetFrameworkMoniker((uint)VSConstants.VSITEMID.Root, out var targetFrameworkMoniker) ||
                string.IsNullOrWhiteSpace(targetFrameworkMoniker))
            {
                return LoaderName;
            }

            try
            {
                var frameworkName = new FrameworkName(targetFrameworkMoniker);

                if (frameworkName.Identifier == ".NETCoreApp" &&
                    frameworkName.Version?.Major >= 3)
                {
199 200 201 202 203 204 205 206 207 208 209 210
                    if (!(_oleServiceProvider.QueryService<SVsShell>() is IVsShell shell))
                    {
                        return null;
                    }

                    var newWinFormsDesignerPackage = new Guid("c78ca057-cc29-421f-ad6d-3b0943debdfc");
                    if (!ErrorHandler.Succeeded(shell.IsPackageInstalled(newWinFormsDesignerPackage, out var installed))
                        || installed == 0)
                    {
                        return null;
                    }

D
Dustin Campbell 已提交
211 212 213 214 215 216 217 218 219 220 221 222
                    return NewLoaderName;
                }
            }
            catch
            {
                // Fall back to the old loader name if there are any failures 
                // while parsing the TFM.
            }

            return LoaderName;
        }

223
        public int MapLogicalView(ref Guid rguidLogicalView, out string? pbstrPhysicalView)
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        {
            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)
        {
            return VSConstants.S_OK;
        }

        int IVsEditorFactoryNotify.NotifyItemAdded(uint grfEFN, IVsHierarchy pHier, uint itemid, string pszMkDocument)
        {
            // Is this being added from a template?
259
            if (((__EFNFLAGS)grfEFN & __EFNFLAGS.EFN_ClonedFromTemplate) != 0)
260
            {
261
                var waitIndicator = _componentModel.GetService<IWaitIndicator>();
262
                // TODO(cyrusn): Can this be cancellable?
263
                waitIndicator.Wait(
264 265
                    "Intellisense",
                    allowCancel: false,
266
                    action: c => FormatDocumentCreatedFromTemplate(pHier, itemid, pszMkDocument, c.CancellationToken));
267 268 269 270 271 272 273 274 275 276
            }

            return VSConstants.S_OK;
        }

        int IVsEditorFactoryNotify.NotifyItemRenamed(IVsHierarchy pHier, uint itemid, string pszMkDocumentOld, string pszMkDocumentNew)
        {
            return VSConstants.S_OK;
        }

277
        private void FormatDocumentCreatedFromTemplate(IVsHierarchy hierarchy, uint itemid, string filePath, CancellationToken cancellationToken)
278
        {
279 280 281
            // 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.
282
            var workspace = _componentModel.GetService<VisualStudioWorkspace>();
283
            var solution = workspace.CurrentSolution;
284

285
            ProjectId? projectIdToAddTo = null;
286

287
            foreach (var projectId in solution.ProjectIds)
288
            {
289
                if (workspace.GetHierarchy(projectId) == hierarchy)
290
                {
291 292 293 294
                    projectIdToAddTo = projectId;
                    break;
                }
            }
295

296 297 298 299 300 301 302 303 304 305 306
            if (projectIdToAddTo == null)
            {
                // We don't have a project for this, so we'll just make up a fake project altogether
                var temporaryProject = solution.AddProject(
                    name: nameof(FormatDocumentCreatedFromTemplate),
                    assemblyName: nameof(FormatDocumentCreatedFromTemplate),
                    language: LanguageName);

                solution = temporaryProject.Solution;
                projectIdToAddTo = temporaryProject.Id;
            }
307

308 309
            var documentId = DocumentId.CreateNewId(projectIdToAddTo);
            var forkedSolution = solution.AddDocument(DocumentInfo.Create(documentId, filePath, loader: new FileTextLoader(filePath, defaultEncoding: null), filePath: filePath));
310
            var addedDocument = forkedSolution.GetDocument(documentId)!;
311

C
CyrusNajmabadi 已提交
312
            var rootToFormat = addedDocument.GetSyntaxRootSynchronously(cancellationToken);
C
Carol Hu 已提交
313
            var documentOptions = ThreadHelper.JoinableTaskFactory.Run(() => addedDocument.GetOptionsAsync(cancellationToken));
314

J
Jason Malinowski 已提交
315
            var formattedTextChanges = Formatter.GetFormattedTextChanges(rootToFormat, workspace, documentOptions, cancellationToken);
316
            var formattedText = addedDocument.GetTextSynchronously(cancellationToken).WithChanges(formattedTextChanges);
317

318
            // Ensure the line endings are normalized. The formatter doesn't touch everything if it doesn't need to.
319
            var targetLineEnding = documentOptions.GetOption(FormattingOptions.NewLine);
320

321 322 323
            var originalText = formattedText;
            foreach (var originalLine in originalText.Lines)
            {
324
                var originalNewLine = originalText.ToString(CodeAnalysis.Text.TextSpan.FromBounds(originalLine.End, originalLine.EndIncludingLineBreak));
325 326 327 328 329 330 331

                // 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));
332 333
                }
            }
334 335 336

            IOUtilities.PerformIO(() =>
            {
C
Cyrus Najmabadi 已提交
337 338 339
                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);
340
            });
341 342 343
        }
    }
}