// ProjectFile.cs // // Author: // Lluis Sanchez Gual // Viktoria Dudka // // Copyright (c) 2009 Novell, Inc (http://www.novell.com) // Copyright (c) 2009 RemObjects Software // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // // using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using Microsoft.CodeAnalysis; using MonoDevelop.Core; using MonoDevelop.Projects.MSBuild; using MonoDevelop.Projects.Policies; namespace MonoDevelop.Projects { public enum Subtype { Code, Designer, Directory } /// /// This class represent a file information in an IProject object. /// public class ProjectFile : ProjectItem, ICloneable, IFileItem, IDisposable { public ProjectFile () { } public ProjectFile (string filename) : this (filename, MonoDevelop.Projects.BuildAction.Compile) { } public ProjectFile (string filename, string buildAction) : this (filename, buildAction, Subtype.Code) { } public ProjectFile (string filename, string buildAction, Subtype subtype) { this.filename = FileService.GetFullPath (filename); this.subtype = subtype; BuildAction = buildAction; } string cachedInclude; public override string Include { get { if (Project != null) { if (cachedInclude != null) return cachedInclude; string path = MSBuildProjectService.ToMSBuildPath (Project.ItemDirectory, FilePath); if (path.Length > 0) { //directory paths must end with '/' if ((Subtype == Subtype.Directory) && path [path.Length - 1] != '\\') path = path + "\\"; // Cache the include path to avoid recalculating MSBuildProjectService.ToMSBuildPath // which can slow down saving SDK style projects that contain thousands of files. cachedInclude = path; return path; } } return base.Include; } protected set { base.Include = value; } } internal protected override void Read (Project project, IMSBuildItemEvaluated buildItem) { base.Read (project, buildItem); if (buildItem.Name == "Folder") { // Read folders string path = MSBuildProjectService.FromMSBuildPath (project.ItemDirectory, buildItem.Include); Name = Path.GetDirectoryName (path); Subtype = Subtype.Directory; return; } Name = MSBuildProjectService.FromMSBuildPath (project.ItemDirectory, buildItem.Include); BuildAction = buildItem.Name; DependsOn = buildItem.Metadata.GetPathValue ("DependentUpon", relativeToPath:FilePath.ParentDirectory); string copy = buildItem.Metadata.GetValue ("CopyToOutputDirectory"); if (!string.IsNullOrEmpty (copy)) { switch (copy) { case "None": break; case "Always": CopyToOutputDirectory = FileCopyMode.Always; break; case "PreserveNewest": CopyToOutputDirectory = FileCopyMode.PreserveNewest; break; default: LoggingService.LogWarning ( "Unrecognised value {0} for CopyToOutputDirectory MSBuild property", copy); break; } } Visible = buildItem.Metadata.GetValue ("Visible", true); resourceId = buildItem.Metadata.GetValue ("LogicalName"); contentType = buildItem.Metadata.GetValue ("SubType"); generator = buildItem.Metadata.GetValue ("Generator"); customToolNamespace = buildItem.Metadata.GetValue ("CustomToolNamespace"); lastGenOutput = buildItem.Metadata.GetValue ("LastGenOutput"); Link = buildItem.Metadata.GetPathValue ("Link", relativeToProject:false); } internal protected override void Write (Project project, MSBuildItem buildItem) { base.Write (project, buildItem); buildItem.Metadata.SetValue ("DependentUpon", DependsOn, FilePath.Empty, relativeToPath:FilePath.ParentDirectory); buildItem.Metadata.SetValue ("SubType", ContentType, ""); buildItem.Metadata.SetValue ("Generator", Generator, ""); buildItem.Metadata.SetValue ("CustomToolNamespace", CustomToolNamespace, ""); buildItem.Metadata.SetValue ("LastGenOutput", LastGenOutput, ""); buildItem.Metadata.SetValue ("Link", Link, FilePath.Empty, relativeToProject:false); buildItem.Metadata.SetValue ("CopyToOutputDirectory", CopyToOutputDirectory.ToString (), "None"); buildItem.Metadata.SetValue ("Visible", Visible, true); var resId = ResourceId; // For EmbeddedResource, emit LogicalName only when it does not match the default msbuild resource Id if (project is DotNetProject && BuildAction == MonoDevelop.Projects.BuildAction.EmbeddedResource && ((DotNetProject)project).GetDefaultMSBuildResourceId (this) == resId) resId = ""; buildItem.Metadata.SetValue ("LogicalName", resId, ""); } Subtype subtype; public Subtype Subtype { get { return subtype; } set { subtype = value; if (subtype == Subtype.Directory) ItemName = "Folder"; OnChanged ("Subtype"); } } public string Name { get { return filename; } set { Debug.Assert (!String.IsNullOrEmpty (value)); FilePath oldPath = filename; FilePath oldLink = Link; filename = FileService.GetFullPath (value); if (HasChildren) { foreach (ProjectFile projectFile in DependentChildren) { if (!string.IsNullOrEmpty (projectFile.dependsOn)) projectFile.dependsOn = Path.GetFileName (FilePath); } } // If the file is a link, rename the link too if (IsLink && Link.FileName == oldPath.FileName) link = Path.Combine (Path.GetDirectoryName (link), filename.FileName); cachedInclude = null; // If a file that belongs to a project is being renamed, update the value of UnevaluatedInclude // since that is used when saving if (Project != null) UnevaluatedInclude = Include; OnPathChanged (oldPath, oldLink); Project?.NotifyFileRenamedInProject (new ProjectFileRenamedEventArgs (Project, this, oldPath)); } } public string BuildAction { get { return ItemName; } set { ItemName = string.IsNullOrEmpty (value) ? MonoDevelop.Projects.BuildAction.None : value; OnChanged ("BuildAction"); } } string resourceId = String.Empty; /// /// Gets the resource id of this file for the provided policy /// internal string GetResourceId (ResourceNamePolicy policy) { if (string.IsNullOrEmpty (resourceId) && (Project is DotNetProject dnp)) return dnp.GetDefaultResourceIdForPolicy (this, policy); return resourceId; } FilePath filename; public FilePath FilePath { get { return filename; } } FilePath IFileItem.FileName { get { return FilePath; } } /// /// The file should be treated as effectively having this relative path within the project. If the file is /// a link or outside the project root, this will not be the same as the physical file. /// public FilePath ProjectVirtualPath => GetProjectVirtualPath (Link, FilePath, Project); static FilePath GetProjectVirtualPath (FilePath link, FilePath filePath, Project project) { if (!link.IsNullOrEmpty) return link; if (project != null) { var rel = project.GetRelativeChildPath (filePath); if (!rel.ToString ().StartsWith ("..", StringComparison.Ordinal)) return rel; } return filePath.FileName; } string contentType; public string ContentType { get { return contentType ?? ""; } set { contentType = value; OnChanged ("ContentType"); } } bool visible = true; /// /// Whether the file should be shown to the user. /// public bool Visible { get { return visible; } set { if (visible != value) { visible = value; OnChanged ("Visible"); } } } string generator; /// /// The ID of a custom code generator. /// public string Generator { get { return generator ?? ""; } set { if (generator != value) { generator = value; OnChanged ("Generator"); } } } string customToolNamespace; /// /// Overrides the namespace in which the custom code generator should generate code. /// public string CustomToolNamespace { get { return customToolNamespace ?? ""; } set { if (customToolNamespace != value) { customToolNamespace = value; OnChanged ("CustomToolNamespace"); } } } string lastGenOutput; /// /// The file most recently generated by the custom tool. Relative to this file's parent directory. /// public string LastGenOutput { get { return lastGenOutput ?? ""; } set { if (lastGenOutput != value) { lastGenOutput = value; OnChanged ("LastGenOutput"); } } } string link; /// /// If the file's real path is outside the project root, this value can be used to set its virtual path /// within the project root. Use ProjectVirtualPath to read the effective virtual path for any file. /// public FilePath Link { get { return link ?? ""; } set { if (link != value) { if (value.IsAbsolute || value.ToString ().StartsWith ("..", StringComparison.Ordinal)) throw new ArgumentException ("Invalid value for Link property"); var oldLink = link; link = value; VirtualPathChanged?.Invoke (this, new ProjectFileVirtualPathChangedEventArgs (this, oldLink, link)); OnChanged ("Link"); } } } /// /// Whether the file is a link. /// public bool IsLink { get { return !Link.IsNullOrEmpty || (Project != null && !FilePath.IsChildPathOf (Project.BaseDirectory)); } } /// /// Whether the file is outside the project base directory. /// public bool IsExternalToProject { get { return !FilePath.IsChildPathOf (Project.BaseDirectory); } } FileCopyMode copyToOutputDirectory = FileCopyMode.None; public FileCopyMode CopyToOutputDirectory { get { return copyToOutputDirectory; } set { if (copyToOutputDirectory != value) { copyToOutputDirectory = value; OnChanged ("CopyToOutputDirectory"); } } } #region File grouping string dependsOn; public string DependsOn { get { return dependsOn ?? ""; } set { if (dependsOn != value) { var oldPath = !string.IsNullOrEmpty (dependsOn) ? FilePath.ParentDirectory.Combine (Path.GetFileName (dependsOn)) : FilePath.Empty; dependsOn = value; if (dependsOnFile != null) { dependsOnFile.dependentChildren.Remove (this); dependsOnFile = null; } if (Project != null && value != null) Project.UpdateDependency (this, oldPath); OnChanged ("DependsOn"); } } } ProjectFile dependsOnFile; public ProjectFile DependsOnFile { get { return dependsOnFile; } internal set { dependsOnFile = value; } } List dependentChildren; public bool HasChildren { get { return dependentChildren != null && dependentChildren.Count > 0; } } public IList DependentChildren { get { return ((IList)dependentChildren) ?? new ProjectFile[0]; } } internal FilePath DependencyPath { get { return FilePath.ParentDirectory.Combine (Path.GetFileName (DependsOn)); } } internal bool ResolveParent (ProjectFile potentialParentFile) { if (potentialParentFile.FilePath == DependencyPath) { dependsOnFile = potentialParentFile; if (dependsOnFile.dependentChildren == null) dependsOnFile.dependentChildren = new List (); dependsOnFile.dependentChildren.Add (this); return true; } return false; } internal bool ResolveParent () { if (dependsOnFile == null && (!string.IsNullOrEmpty (dependsOn) && Project != null)) { //NOTE also that the dependent files are always assumed to be in the same directory //This matches VS behaviour var parentPath = DependencyPath; //don't allow cyclic references if (parentPath == FilePath) { LoggingService.LogWarning ( "Cyclic dependency in project '{0}': file '{1}' depends on '{2}'", Project == null ? "(none)" : Project.Name, FilePath, parentPath ); return true; } dependsOnFile = Project.Files.GetFile (parentPath); if (dependsOnFile != null) { if (dependsOnFile.dependentChildren == null) dependsOnFile.dependentChildren = new List (); dependsOnFile.dependentChildren.Add (this); return true; } } return false; } #endregion // FIXME: rename this to LogicalName for a better mapping to the MSBuild property public string ResourceId { get { // If the resource id is not set, return the project's default if (BuildAction == MonoDevelop.Projects.BuildAction.EmbeddedResource && string.IsNullOrEmpty (resourceId) && Project is DotNetProject dnp) return dnp.GetDefaultResourceId (this); return resourceId; } set { if (resourceId != value) { var oldVal = ResourceId; resourceId = value; if (ResourceId != oldVal) OnChanged ("ResourceId"); } } } Project project; protected override void OnProjectSet () { base.OnProjectSet (); if (project != null) { project.Modified -= OnProjectModified; project = null; } if (Project != null) { base.Include = Include; project = Project; project.Modified += OnProjectModified; VirtualPathChanged?.Invoke (this, new ProjectFileVirtualPathChangedEventArgs (this, FilePath.Null, ProjectVirtualPath)); } } void OnProjectModified (object sender, SolutionItemModifiedEventArgs e) { foreach (var eventInfo in e) { if (eventInfo.Hint == "FileName") { cachedInclude = null; return; } } } public override string ToString () { return "[ProjectFile: FileName=" + filename + "]"; } public object Clone () { ProjectFile pf = (ProjectFile)MemberwiseClone (); pf.dependsOnFile = null; pf.dependentChildren = null; pf.Project = null; pf.VirtualPathChanged = null; pf.PathChanged = null; pf.BackingItem = null; pf.BackingEvalItem = null; return pf; } public void Dispose () => OnDispose (); protected virtual void OnDispose () { if (project != null) { project.Modified -= OnProjectModified; project = null; } } internal event EventHandler VirtualPathChanged; internal event EventHandler PathChanged; void OnPathChanged (FilePath oldPath, FilePath oldLink) { PathChanged?.Invoke (this, CreateEventArgs ()); ProjectFilePathChangedEventArgs CreateEventArgs () { var oldVirtualPath = GetProjectVirtualPath (oldLink, oldPath, Project); return new ProjectFilePathChangedEventArgs (this, oldPath, filename, oldVirtualPath, ProjectVirtualPath); } } protected virtual void OnChanged (string property) { Project?.NotifyFilePropertyChangedInProject (this, property); } public virtual SourceCodeKind SourceCodeKind => filename.HasExtension (".csx") || filename.HasExtension (".vbx") ? SourceCodeKind.Script : SourceCodeKind.Regular; } class ProjectFileVirtualPathChangedEventArgs : EventArgs { public ProjectFileVirtualPathChangedEventArgs (ProjectFile projectFile, FilePath oldPath, FilePath newPath) { ProjectFile = projectFile; OldVirtualPath = oldPath; NewVirtualPath = newPath; } public ProjectFile ProjectFile { get; } public FilePath OldVirtualPath { get; } public FilePath NewVirtualPath { get; } } class ProjectFilePathChangedEventArgs : ProjectFileVirtualPathChangedEventArgs { public ProjectFilePathChangedEventArgs (ProjectFile projectFile, FilePath oldPath, FilePath newPath, FilePath oldVirtualPath, FilePath newVirtualPath) : base (projectFile, oldVirtualPath, newVirtualPath) { OldPath = oldPath; NewPath = newPath; } public FilePath OldPath { get; } public FilePath NewPath { get; } } }