|
// 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;
}
}
}
|