// 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.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Editor.Host; using Microsoft.CodeAnalysis.Editor.Shared.Extensions; using Microsoft.CodeAnalysis.FileHeaders; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.Utilities; using Microsoft.CodeAnalysis.Text; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Designer.Interfaces; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Utilities; using Roslyn.Utilities; namespace Microsoft.VisualStudio.LanguageServices.Implementation { /// /// The base class of both the Roslyn editor factories. /// internal abstract class AbstractEditorFactory : IVsEditorFactory, 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; } protected abstract SyntaxGenerator SyntaxGenerator { get; } protected abstract AbstractFileHeaderHelper FileHeaderHelper { get; } 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) { 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(); // Do we need to create a text buffer? if (textBuffer == null) { var contentTypeRegistryService = _componentModel.GetService(); var contentType = contentTypeRegistryService.GetContentType(ContentTypeName); 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); 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. 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": // We must create the WinForms designer here var loaderName = GetWinFormsLoaderName(vsHierarchy); var designerService = (IVSMDDesignerService)_oleServiceProvider.QueryService(); var designerLoader = (IVSMDDesignerLoader)designerService.CreateDesignerLoader(loaderName); if (designerLoader is null) { goto case "Code"; } 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": 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; } private string? GetWinFormsLoaderName(IVsHierarchy vsHierarchy) { 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) { return NewLoaderName; } } catch { // Fall back to the old loader name if there are any failures // while parsing the TFM. } return LoaderName; } 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 waitIndicator = _componentModel.GetService(); // TODO(cyrusn): Can this be cancellable? waitIndicator.Wait( "Intellisense", allowCancel: false, action: c => FormatDocumentCreatedFromTemplate(pHier, itemid, pszMkDocument, c.CancellationToken)); } return VSConstants.S_OK; } int IVsEditorFactoryNotify.NotifyItemRenamed(IVsHierarchy pHier, uint itemid, string pszMkDocumentOld, string pszMkDocumentNew) => VSConstants.S_OK; protected virtual Task OrganizeUsingsCreatedFromTemplateAsync(Document document, CancellationToken cancellationToken) => Formatter.OrganizeImportsAsync(document, cancellationToken); private void FormatDocumentCreatedFromTemplate(IVsHierarchy hierarchy, uint itemid, 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(); var solution = workspace.CurrentSolution; ProjectId? projectIdToAddTo = null; foreach (var projectId in solution.ProjectIds) { if (workspace.GetHierarchy(projectId) == hierarchy) { projectIdToAddTo = projectId; break; } } 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; } var documentId = DocumentId.CreateNewId(projectIdToAddTo); var forkedSolution = solution.AddDocument(DocumentInfo.Create(documentId, filePath, loader: new FileTextLoader(filePath, defaultEncoding: null), filePath: filePath)); var addedDocument = forkedSolution.GetDocument(documentId)!; var rootToFormat = addedDocument.GetSyntaxRootSynchronously(cancellationToken); Contract.ThrowIfNull(rootToFormat); var documentOptions = ThreadHelper.JoinableTaskFactory.Run(() => addedDocument.GetOptionsAsync(cancellationToken)); // Apply file header preferences var fileHeaderTemplate = documentOptions.GetOption(CodeStyleOptions2.FileHeaderTemplate); if (!string.IsNullOrEmpty(fileHeaderTemplate)) { var documentWithFileHeader = ThreadHelper.JoinableTaskFactory.Run(() => { var newLineText = documentOptions.GetOption(FormattingOptions.NewLine, rootToFormat.Language); var newLineTrivia = SyntaxGenerator.EndOfLine(newLineText); return AbstractFileHeaderCodeFixProvider.GetTransformedSyntaxRootAsync( SyntaxGenerator.SyntaxFacts, FileHeaderHelper, newLineTrivia, addedDocument, cancellationToken); }); addedDocument = addedDocument.WithSyntaxRoot(documentWithFileHeader); rootToFormat = documentWithFileHeader; } // Organize using directives addedDocument = ThreadHelper.JoinableTaskFactory.Run(() => OrganizeUsingsCreatedFromTemplateAsync(addedDocument, cancellationToken)); rootToFormat = ThreadHelper.JoinableTaskFactory.Run(() => addedDocument.GetRequiredSyntaxRootAsync(cancellationToken).AsTask()); // Format document var unformattedText = addedDocument.GetTextSynchronously(cancellationToken); var formattedRoot = Formatter.Format(rootToFormat, workspace, documentOptions, 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 = documentOptions.GetOption(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); }); } } }