// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Host;
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel;
using Microsoft.VisualStudio.LanguageServices.Implementation.EditAndContinue;
using Microsoft.VisualStudio.LanguageServices.Implementation.TaskList;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem.Legacy
{
///
/// Base type for legacy C# and VB project system shim implementations.
/// These legacy shims are based on legacy project system interfaces defined in csproj/msvbprj.
///
internal abstract partial class AbstractLegacyProject : ForegroundThreadAffinitizedObject
{
public IVsHierarchy Hierarchy { get; }
protected VisualStudioProject VisualStudioProject { get; }
internal VisualStudioProjectOptionsProcessor VisualStudioProjectOptionsProcessor { get; set; }
protected IProjectCodeModel ProjectCodeModel { get; set; }
protected VisualStudioWorkspace Workspace { get; }
internal VisualStudioProject Test_VisualStudioProject => VisualStudioProject;
///
/// The path to the directory of the project. Read-only, since although you can rename
/// a project in Visual Studio you can't change the folder of a project without an
/// unload/reload.
///
private readonly string _projectDirectory = null;
private static readonly char[] PathSeparatorCharacters = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
#region Mutable fields that should only be used from the UI thread
private readonly SolutionEventsBatchScopeCreator _batchScopeCreator;
#endregion
public AbstractLegacyProject(
string projectSystemName,
IVsHierarchy hierarchy,
string language,
IServiceProvider serviceProvider,
IThreadingContext threadingContext,
string externalErrorReportingPrefix,
HostDiagnosticUpdateSource hostDiagnosticUpdateSourceOpt,
ICommandLineParserService commandLineParserServiceOpt)
: base(threadingContext, assertIsForeground: true)
{
Contract.ThrowIfNull(hierarchy);
var componentModel = (IComponentModel)serviceProvider.GetService(typeof(SComponentModel));
Workspace = componentModel.GetService();
var workspaceImpl = (VisualStudioWorkspaceImpl)Workspace;
var projectFilePath = hierarchy.TryGetProjectFilePath();
if (projectFilePath != null && !File.Exists(projectFilePath))
{
projectFilePath = null;
}
if (projectFilePath != null)
{
_projectDirectory = Path.GetDirectoryName(projectFilePath);
}
var projectFactory = componentModel.GetService();
VisualStudioProject = projectFactory.CreateAndAddToWorkspace(
projectSystemName,
language,
new VisualStudioProjectCreationInfo
{
// The workspace requires an assembly name so we can make compilations. We'll use
// projectSystemName because they'll have a better one eventually.
AssemblyName = projectSystemName,
FilePath = projectFilePath,
Hierarchy = hierarchy,
ProjectGuid = GetProjectIDGuid(hierarchy),
});
workspaceImpl.AddProjectRuleSetFileToInternalMaps(
VisualStudioProject,
() => VisualStudioProjectOptionsProcessor.EffectiveRuleSetFilePath);
// Right now VB doesn't have the concept of "default namespace". But we conjure one in workspace
// by assigning the value of the project's root namespace to it. So various feature can choose to
// use it for their own purpose.
// In the future, we might consider officially exposing "default namespace" for VB project
// (e.g. through a msbuild property)
VisualStudioProject.DefaultNamespace = GetRootNamespacePropertyValue(hierarchy);
if (TryGetMaxLangVersionPropertyValue(hierarchy, out var maxLangVer))
{
VisualStudioProject.MaxLangVersion = maxLangVer;
}
Hierarchy = hierarchy;
ConnectHierarchyEvents();
RefreshBinOutputPath();
workspaceImpl.SubscribeExternalErrorDiagnosticUpdateSourceToSolutionBuildEvents();
_externalErrorReporter = new ProjectExternalErrorReporter(VisualStudioProject.Id, externalErrorReportingPrefix, workspaceImpl);
_batchScopeCreator = componentModel.GetService();
_batchScopeCreator.StartTrackingProject(VisualStudioProject, Hierarchy);
}
public string AssemblyName => VisualStudioProject.AssemblyName;
public string GetOutputFileName()
=> VisualStudioProject.IntermediateOutputFilePath;
public virtual void Disconnect()
{
_batchScopeCreator.StopTrackingProject(VisualStudioProject);
VisualStudioProjectOptionsProcessor?.Dispose();
ProjectCodeModel.OnProjectClosed();
VisualStudioProject.RemoveFromWorkspace();
// Unsubscribe IVsHierarchyEvents
DisconnectHierarchyEvents();
}
protected void AddFile(
string filename,
SourceCodeKind sourceCodeKind)
{
AssertIsForeground();
// We have tests that assert that XOML files should not get added; this was similar
// behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
// them. XOML support is planned to go away for Dev16, but for now leave the logic there.
if (filename.EndsWith(".xoml"))
{
return;
}
ImmutableArray folders = default;
var itemid = Hierarchy.TryGetItemId(filename);
if (itemid != VSConstants.VSITEMID_NIL)
{
folders = GetFolderNamesForDocument(itemid);
}
VisualStudioProject.AddSourceFile(filename, sourceCodeKind, folders);
}
protected void AddFile(
string filename,
string linkMetadata,
SourceCodeKind sourceCodeKind)
{
// We have tests that assert that XOML files should not get added; this was similar
// behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
// them. XOML support is planned to go away for Dev16, but for now leave the logic there.
if (filename.EndsWith(".xoml"))
{
return;
}
var folders = ImmutableArray.Empty;
if (!string.IsNullOrEmpty(linkMetadata))
{
var linkFolderPath = Path.GetDirectoryName(linkMetadata);
folders = linkFolderPath.Split(PathSeparatorCharacters, StringSplitOptions.RemoveEmptyEntries).ToImmutableArray();
}
else if (!string.IsNullOrEmpty(VisualStudioProject.FilePath))
{
var relativePath = PathUtilities.GetRelativePath(_projectDirectory, filename);
var relativePathParts = relativePath.Split(PathSeparatorCharacters);
folders = ImmutableArray.Create(relativePathParts, start: 0, length: relativePathParts.Length - 1);
}
VisualStudioProject.AddSourceFile(filename, sourceCodeKind, folders);
}
protected void RemoveFile(string filename)
{
// We have tests that assert that XOML files should not get added; this was similar
// behavior to how ASP.NET projects would add .aspx files even though we ultimately ignored
// them. XOML support is planned to go away for Dev16, but for now leave the logic there.
if (filename.EndsWith(".xoml"))
{
return;
}
VisualStudioProject.RemoveSourceFile(filename);
ProjectCodeModel.OnSourceFileRemoved(filename);
}
protected void RefreshBinOutputPath()
{
if (!(Hierarchy is IVsBuildPropertyStorage storage))
{
return;
}
if (ErrorHandler.Failed(storage.GetPropertyValue("OutDir", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var outputDirectory)) ||
ErrorHandler.Failed(storage.GetPropertyValue("TargetFileName", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var targetFileName)))
{
return;
}
if (targetFileName == null)
{
return;
}
// web app case
if (!PathUtilities.IsAbsolute(outputDirectory))
{
if (VisualStudioProject.FilePath == null)
{
return;
}
outputDirectory = FileUtilities.ResolveRelativePath(outputDirectory, Path.GetDirectoryName(VisualStudioProject.FilePath));
}
if (outputDirectory == null)
{
return;
}
VisualStudioProject.OutputFilePath = FileUtilities.NormalizeAbsolutePath(Path.Combine(outputDirectory, targetFileName));
if (ErrorHandler.Succeeded(storage.GetPropertyValue("TargetRefPath", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out var targetRefPath)) && !string.IsNullOrEmpty(targetRefPath))
{
VisualStudioProject.OutputRefFilePath = targetRefPath;
}
else
{
VisualStudioProject.OutputRefFilePath = null;
}
}
private static Guid GetProjectIDGuid(IVsHierarchy hierarchy)
{
if (hierarchy.TryGetGuidProperty(__VSHPROPID.VSHPROPID_ProjectIDGuid, out var guid))
{
return guid;
}
return Guid.Empty;
}
private static bool GetIsWebsiteProject(IVsHierarchy hierarchy)
{
try
{
if (hierarchy.TryGetProject(out var project))
{
return project.Kind == VsWebSite.PrjKind.prjKindVenusProject;
}
}
catch (Exception)
{
}
return false;
}
///
/// Map of folder item IDs in the workspace to the string version of their path.
///
/// Using item IDs as a key like this in a long-lived way is considered unsupported by CPS and other
/// IVsHierarchy providers, but this code (which is fairly old) still makes the assumptions anyways.
private readonly Dictionary> _folderNameMap = new Dictionary>();
private ImmutableArray GetFolderNamesForDocument(uint documentItemID)
{
AssertIsForeground();
if (documentItemID != (uint)VSConstants.VSITEMID.Nil && Hierarchy.GetProperty(documentItemID, (int)VsHierarchyPropID.Parent, out var parentObj) == VSConstants.S_OK)
{
var parentID = UnboxVSItemId(parentObj);
if (parentID != (uint)VSConstants.VSITEMID.Nil && parentID != (uint)VSConstants.VSITEMID.Root)
{
return GetFolderNamesForFolder(parentID);
}
}
return ImmutableArray.Empty;
}
private ImmutableArray GetFolderNamesForFolder(uint folderItemID)
{
AssertIsForeground();
using var pooledObject = SharedPools.Default>().GetPooledObject();
var newFolderNames = pooledObject.Object;
if (!_folderNameMap.TryGetValue(folderItemID, out var folderNames))
{
ComputeFolderNames(folderItemID, newFolderNames, Hierarchy);
folderNames = newFolderNames.ToImmutableArray();
_folderNameMap.Add(folderItemID, folderNames);
}
else
{
// verify names, and change map if we get a different set.
// this is necessary because we only get document adds/removes from the project system
// when a document name or folder name changes.
ComputeFolderNames(folderItemID, newFolderNames, Hierarchy);
if (!Enumerable.SequenceEqual(folderNames, newFolderNames))
{
folderNames = newFolderNames.ToImmutableArray();
_folderNameMap[folderItemID] = folderNames;
}
}
return folderNames;
}
// Different hierarchies are inconsistent on whether they return ints or uints for VSItemIds.
// Technically it should be a uint. However, there's no enforcement of this, and marshalling
// from native to managed can end up resulting in boxed ints instead. Handle both here so
// we're resilient to however the IVsHierarchy was actually implemented.
private static uint UnboxVSItemId(object id)
{
return id is uint ? (uint)id : unchecked((uint)(int)id);
}
private static void ComputeFolderNames(uint folderItemID, List names, IVsHierarchy hierarchy)
{
if (hierarchy.GetProperty((uint)folderItemID, (int)VsHierarchyPropID.Name, out var nameObj) == VSConstants.S_OK)
{
// For 'Shared' projects, IVSHierarchy returns a hierarchy item with < character in its name (i.e. )
// as a child of the root item. There is no such item in the 'visual' hierarchy in solution explorer and no such folder
// is present on disk either. Since this is not a real 'folder', we exclude it from the contents of Document.Folders.
// Note: The parent of the hierarchy item that contains < character in its name is VSITEMID.Root. So we don't need to
// worry about accidental propagation out of the Shared project to any containing 'Solution' folders - the check for
// VSITEMID.Root below already takes care of that.
var name = (string)nameObj;
if (!name.StartsWith("<", StringComparison.OrdinalIgnoreCase))
{
names.Insert(0, name);
}
}
if (hierarchy.GetProperty((uint)folderItemID, (int)VsHierarchyPropID.Parent, out var parentObj) == VSConstants.S_OK)
{
var parentID = UnboxVSItemId(parentObj);
if (parentID != (uint)VSConstants.VSITEMID.Nil && parentID != (uint)VSConstants.VSITEMID.Root)
{
ComputeFolderNames(parentID, names, hierarchy);
}
}
}
///
/// Get the value of "rootnamespace" property of the project ("" if not defined, which means global namespace),
/// or null if it is unknown or not applicable.
///
///
/// This property has different meaning between C# and VB, each project type can decide how to interpret the value.
/// >
private static string GetRootNamespacePropertyValue(IVsHierarchy hierarchy)
{
// While both csproj and vbproj might define property in the project file,
// they are very different things.
//
// In C#, it's called default namespace (even though we got the value from rootnamespace property),
// and it doesn't affect the semantic of the code in anyway, just something used by VS.
// For example, when you create a new class, the namespace for the new class is based on it.
// Therefore, we can't get this info from compiler.
//
// However, in VB, it's actually called root namespace, and that info is part of the VB compilation
// (parsed from arguments), because VB compiler needs it to determine the root of all the namespace
// declared in the compilation.
//
// Unfortunately, although being different concepts, default namespace and root namespace are almost
// used interchangeably in VS. For example, (1) the value is define in "rootnamespace" property in project
// files and, (2) the property name we use to call into hierarchy below to retrieve the value is
// called "DefaultNamespace".
if (hierarchy.TryGetProperty(__VSHPROPID.VSHPROPID_DefaultNamespace, out string value))
{
return value;
}
return null;
}
private static bool TryGetMaxLangVersionPropertyValue(IVsHierarchy hierarchy, out string maxLangVer)
{
if (!(hierarchy is IVsBuildPropertyStorage storage))
{
maxLangVer = null;
return false;
}
return ErrorHandler.Succeeded(storage.GetPropertyValue("MaxSupportedLangVersion", null, (uint)_PersistStorageType.PST_PROJECT_FILE, out maxLangVer));
}
}
}