/* **************************************************************************** * * Copyright (c) Microsoft Corporation. * * This source code is subject to terms and conditions of the Apache License, Version 2.0. A * copy of the license can be found in the License.html file at the root of this distribution. If * you cannot locate the Apache License, Version 2.0, please send an email to * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound * by the terms of the Apache License, Version 2.0. * * You must not remove this notice, or any other, from this software. * * ***************************************************************************/ using System; using System.Collections.Generic; using System.ComponentModel.Design; using System.Diagnostics; using System.Diagnostics.Contracts; using System.Drawing; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Windows.Threading; using Microsoft.VisualStudio; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Navigation; using Microsoft.VisualStudio.Project.Automation; using MSBuild = Microsoft.Build.Evaluation; using VsCommands2K = Microsoft.VisualStudio.VSConstants.VSStd2KCmdID; using VSConstants = Microsoft.VisualStudio.VSConstants; namespace Microsoft.VisualStudio.Project { public enum CommonImageName { File = 0, Project = 1, SearchPathContainer, SearchPath, MissingSearchPath, StartupFile, InterpretersContainer = SearchPathContainer, Interpreter = SearchPath, InterpretersPackage = SearchPath } public abstract class CommonProjectNode : ProjectNode, IVsProjectSpecificEditorMap2, IVsDeferredSaveProject { private CommonProjectPackage/*!*/ _package; private Guid _mruPageGuid = new Guid(CommonConstants.AddReferenceMRUPageGuid); private VSLangProj.VSProject _vsProject = null; private static ImageList _imageList; private ProjectDocumentsListenerForStartupFileUpdates _projectDocListenerForStartupFileUpdates; private static int _imageOffset; private FileSystemWatcher _watcher, _attributesWatcher; private int _suppressFileWatcherCount; private bool _isRefreshing; public bool _boldedStartupItem; private bool _showingAllFiles; private object _automationObject; private PropertyPage _propPage; private readonly Dictionary _fileChangedHandlers = new Dictionary(); private Queue _fileSystemChanges = new Queue(); private object _fileSystemChangesLock = new object(); public UIThreadSynchronizer _uiSync; private MSBuild.Project userBuildProject; private readonly Dictionary _symlinkWatchers = new Dictionary(); private DiskMerger _currentMerger; private int _idleTriggered; public CommonProjectNode(CommonProjectPackage/*!*/ package, ImageList/*!*/ imageList) : base(package) { Contract.Assert(package != null); Contract.Assert(imageList != null); _package = package; CanFileNodesHaveChilds = true; OleServiceProvider.AddService(typeof(VSLangProj.VSProject), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false); SupportsProjectDesigner = true; _imageList = imageList; //Store the number of images in ProjectNode so we know the offset of the language icons. _imageOffset = ImageHandler.ImageList.Images.Count; foreach (Image img in ImageList.Images) { ImageHandler.AddImage(img); } InitializeCATIDs(); _uiSync = new UIThreadSynchronizer(); package.OnIdle += OnIdle; } #region abstract methods public abstract Type GetEditorFactoryType(); public abstract string GetProjectName(); public virtual CommonFileNode CreateCodeFileNode(ProjectElement item) { return new CommonFileNode(this, item); } public virtual CommonFileNode CreateNonCodeFileNode(ProjectElement item) { return new CommonNonCodeFileNode(this, item); } public abstract string GetFormatList(); public abstract Type GetLibraryManagerType(); public abstract Type GetProjectFactoryType(); #endregion #region Properties public new CommonProjectPackage/*!*/ Package { get { return _package; } } public static int ImageOffset { get { return _imageOffset; } } /// /// Get the VSProject corresponding to this project /// public VSLangProj.VSProject VSProject { get { if (_vsProject == null) _vsProject = new OAVSProject(this); return _vsProject; } } private IVsHierarchy InteropSafeHierarchy { get { IntPtr unknownPtr = VsUtilities.QueryInterfaceIUnknown(this); if (IntPtr.Zero == unknownPtr) { return null; } IVsHierarchy hier = Marshal.GetObjectForIUnknown(unknownPtr) as IVsHierarchy; return hier; } } /// /// Indicates whether the project is currently is busy refreshing its hierarchy. /// public bool IsRefreshing { get { return _isRefreshing; } set { _isRefreshing = value; } } /// /// Language specific project images /// public static ImageList ImageList { get { return _imageList; } set { _imageList = value; } } public PropertyPage PropertyPage { get { return _propPage; } set { _propPage = value; } } #endregion #region overridden properties public override bool CanShowAllFiles { get { return true; } } public override bool IsShowingAllFiles { get { return _showingAllFiles; } } /// /// Since we appended the language images to the base image list in the constructor, /// this should be the offset in the ImageList of the langauge project icon. /// public override int ImageIndex { get { return _imageOffset + (int)CommonImageName.Project; } } public override Guid ProjectGuid { get { return GetProjectFactoryType().GUID; } } public override string ProjectType { get { return GetProjectName(); } } public override object Object { get { return null; } } #endregion #region overridden methods public override object GetAutomationObject() { if (_automationObject == null) { _automationObject = base.GetAutomationObject(); } return _automationObject; } public override int QueryStatusOnNode(Guid cmdGroup, uint cmd, IntPtr pCmdText, ref QueryStatusResult result) { if (cmdGroup == CommonConstants.Std97CmdGroupGuid) { switch ((VSConstants.VSStd97CmdID)cmd) { case VSConstants.VSStd97CmdID.BuildCtx: case VSConstants.VSStd97CmdID.RebuildCtx: case VSConstants.VSStd97CmdID.CleanCtx: result = QueryStatusResult.SUPPORTED | QueryStatusResult.INVISIBLE; return VSConstants.S_OK; } } else if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) { switch ((VsCommands2K)cmd) { case VsCommands2K.ECMD_PUBLISHSELECTION: if (pCmdText != IntPtr.Zero && NativeMethods.OLECMDTEXT.GetFlags(pCmdText) == NativeMethods.OLECMDTEXT.OLECMDTEXTF.OLECMDTEXTF_NAME) { NativeMethods.OLECMDTEXT.SetText(pCmdText, "Publish " + this.Caption); } if (IsPublishingEnabled) { result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED; } else { result |= QueryStatusResult.SUPPORTED; } return VSConstants.S_OK; case VsCommands2K.ECMD_PUBLISHSLNCTX: if (IsPublishingEnabled) { result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED; } else { result |= QueryStatusResult.SUPPORTED; } return VSConstants.S_OK; case CommonConstants.OpenFolderInExplorerCmdId: result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED; return VSConstants.S_OK; } } else if (cmdGroup == SharedCommandGuid) { switch ((SharedCommands)cmd) { case SharedCommands.AddExistingFolder: result |= QueryStatusResult.SUPPORTED | QueryStatusResult.ENABLED; return VSConstants.S_OK; } } return base.QueryStatusOnNode(cmdGroup, cmd, pCmdText, ref result); } private bool IsPublishingEnabled { get { return !String.IsNullOrWhiteSpace(GetProjectProperty(CommonConstants.PublishUrl)); } } public override int ExecCommandOnNode(Guid cmdGroup, uint cmd, uint nCmdexecopt, IntPtr pvaIn, IntPtr pvaOut) { if (cmdGroup == Microsoft.VisualStudio.Project.VsMenus.guidStandardCommandSet2K) { switch ((VsCommands2K)cmd) { case VsCommands2K.ECMD_PUBLISHSELECTION: case VsCommands2K.ECMD_PUBLISHSLNCTX: Publish(PublishProjectOptions.Default, true); return VSConstants.S_OK; case CommonConstants.OpenFolderInExplorerCmdId: Process.Start(this.ProjectHome); return VSConstants.S_OK; } } else if (cmdGroup == SharedCommandGuid) { switch ((SharedCommands)cmd) { case SharedCommands.AddExistingFolder: return AddExistingFolderToNode(this); } } return base.ExecCommandOnNode(cmdGroup, cmd, nCmdexecopt, pvaIn, pvaOut); } public int AddExistingFolderToNode(HierarchyNode parent) { var dir = BrowseForFolder(String.Format("Add Existing Folder - {0}",Caption), parent.FullPathToChildren); if (dir != null) { DropFilesOrFolders(new[] { dir }, parent); } return VSConstants.S_OK; } /// /// Publishes the project as configured by the user in the Publish option page. /// /// If async is true this function begins the publishing and returns w/o waiting for it to complete. No errors are reported. /// /// If async is false this function waits for the publish to finish and raises a PublishFailedException with an /// inner exception indicating the underlying reason for the publishing failure. /// /// Returns true if the publish was succeessfully started, false if the project is not configured for publishing /// public bool Publish(PublishProjectOptions publishOptions, bool async) { string publishUrl = publishOptions.DestinationUrl ?? GetProjectProperty(CommonConstants.PublishUrl); bool found = false; if (!String.IsNullOrWhiteSpace(publishUrl)) { var url = new Url(publishUrl); var publishers = CommonPackage.ComponentModel.GetExtensions(); foreach (var publisher in publishers) { if (publisher.Schema == url.Uri.Scheme) { var project = new PublishProject(this, publishOptions); Exception failure = null; var frame = new DispatcherFrame(); var thread = new System.Threading.Thread(x => { try { publisher.PublishFiles(project, url.Uri); project.Done(); frame.Continue = false; } catch (Exception e) { failure = e; project.Failed(e.Message); frame.Continue = false; } }); thread.Start(); found = true; if (!async) { Dispatcher.PushFrame(frame); if (failure != null) { throw new PublishFailedException(String.Format("Publishing of the project {0} failed", Caption), failure); } } break; } } if (!found) { var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar)); statusBar.SetText(String.Format("Publish failed: Unknown publish scheme ({0})", url.Uri.Scheme)); } } else { var statusBar = (IVsStatusbar)CommonPackage.GetGlobalService(typeof(SVsStatusbar)); statusBar.SetText(String.Format("Project is not configured for publishing in project properties.")); } return found; } public virtual CommonConfig MakeConfiguration(string activeConfigName, string activePlatformName) { return new CommonConfig(this, activeConfigName, activePlatformName); } /// /// As we don't register files/folders in the project file, removing an item is a noop. /// public override int RemoveItem(uint reserved, uint itemId, out int result) { result = 1; return VSConstants.S_OK; } public override void BuildAsync(uint vsopts, string config, string platform, IVsOutputWindowPane output, string target, IEnumerable files, Action uiThreadCallback) { uiThreadCallback(MSBuildResult.Successful, target); } /// /// Overriding main project loading method to inject our hierarachy of nodes. /// protected override void Reload() { base.Reload(); OnProjectPropertyChanged += CommonProjectNode_OnProjectPropertyChanged; // track file creation/deletes and update our glyphs when files start/stop existing for files in the project. if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Dispose(); } string userProjectFilename = FileName + ".user"; if (File.Exists(userProjectFilename)) { userBuildProject = BuildProject.ProjectCollection.LoadProject(userProjectFilename); } bool? showAllFiles = null; if (userBuildProject != null) { showAllFiles = GetShowAllFilesSetting(userBuildProject.GetPropertyValue(CommonConstants.ProjectView)); } _showingAllFiles = showAllFiles ?? GetShowAllFilesSetting(BuildProject.GetPropertyValue(CommonConstants.ProjectView)) ?? false; _watcher = CreateFileSystemWatcher(ProjectHome); _attributesWatcher = CreateAttributesWatcher(ProjectHome); // add everything that's on disk that we don't have in the project MergeDiskNodes(this, ProjectHome); this.SetProjectFileDirty(false); } private FileSystemWatcher CreateFileSystemWatcher(string dir) { var watcher = new FileSystemWatcher(dir); watcher.IncludeSubdirectories = true; watcher.Created += new FileSystemEventHandler(FileExistanceChanged); watcher.Deleted += new FileSystemEventHandler(FileExistanceChanged); watcher.Renamed += new RenamedEventHandler(FileNameChanged); watcher.Changed += FileContentsChanged; watcher.Error += WatcherError; watcher.EnableRaisingEvents = true; watcher.InternalBufferSize = 1024 * 4; // 4k is minimum buffer size return watcher; } private FileSystemWatcher CreateAttributesWatcher(string dir) { var watcher = new FileSystemWatcher(dir); watcher.NotifyFilter = NotifyFilters.Attributes; watcher.Changed += FileAttributesChanged; watcher.Error += WatcherError; watcher.EnableRaisingEvents = true; watcher.InternalBufferSize = 1024 * 4; // 4k is minimum buffer size return watcher; } /// /// When the file system watcher buffer overflows we need to scan the entire /// directory for changes. /// private void WatcherError(object sender, ErrorEventArgs e) { lock (_fileSystemChanges) { _fileSystemChanges.Clear(); // none of the other changes matter now, we'll rescan the world _currentMerger = null; // abort any current merge now that we have a new one _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.All, null)); TriggerIdle(); } } protected override void SaveMSBuildProjectFileAs(string newFileName) { base.SaveMSBuildProjectFileAs(newFileName); if (userBuildProject != null) { userBuildProject.Save(FileName + ".user"); } } protected override void SaveMSBuildProjectFile(string filename) { base.SaveMSBuildProjectFile(filename); if (userBuildProject != null) { userBuildProject.Save(filename + ".user"); } } protected override void Dispose(bool disposing) { base.Dispose(disposing); if (this.userBuildProject != null) { userBuildProject.ProjectCollection.UnloadProject(userBuildProject); } _package.OnIdle -= OnIdle; } public override int ShowAllFiles() { if (!QueryEditProjectFile(false)) { return VSConstants.E_FAIL; } if (_showingAllFiles) { UpdateShowAllFiles(this, enabled: false); } else { UpdateShowAllFiles(this, enabled: true); ExpandItem(EXPANDFLAGS.EXPF_ExpandFolder); } _showingAllFiles = !_showingAllFiles; string newPropValue = _showingAllFiles ? CommonConstants.ShowAllFiles : CommonConstants.ProjectFiles; var projProperty = BuildProject.GetProperty(CommonConstants.ProjectView); if (projProperty != null && !projProperty.IsImported && !String.IsNullOrWhiteSpace(projProperty.EvaluatedValue)) { // setting is persisted in main project file, update it there. BuildProject.SetProperty(CommonConstants.ProjectView, newPropValue); } else { // save setting in user project file if (userBuildProject == null) { // user project file doesn't exist yet, create it. userBuildProject = new MSBuild.Project(BuildProject.ProjectCollection); userBuildProject.FullPath = FileName + ".user"; } userBuildProject.SetProperty(CommonConstants.ProjectView, newPropValue); } SetProjectFileDirty(true); // update project state return VSConstants.S_OK; } private void UpdateShowAllFiles(HierarchyNode node, bool enabled) { for (var curNode = node.FirstChild; curNode != null; curNode = curNode.NextSibling) { UpdateShowAllFiles(curNode, enabled); var allFiles = curNode.ItemNode as AllFilesProjectElement; if (allFiles != null) { curNode.IsVisible = enabled; OnInvalidateItems(node); } } } private static bool? GetShowAllFilesSetting(string showAllFilesValue) { bool? showAllFiles = null; string showAllFilesSetting = showAllFilesValue.Trim(); if (String.Equals(showAllFilesSetting, CommonConstants.ProjectFiles)) { showAllFiles = false; } else if (String.Equals(showAllFilesSetting, CommonConstants.ShowAllFiles)) { showAllFiles = true; } return showAllFiles; } /// /// Performs merging of the file system state with the current project hierarchy, bringing them /// back into sync. /// /// The class can be created, and ContinueMerge should be called until it returns false, at which /// point the file system has been merged. /// /// You can wait between calls to ContinueMerge to enable not blocking the UI. /// /// If there were changes which came in while the DiskMerger was processing then those changes will still need /// to be processed after the DiskMerger completes. /// class DiskMerger { private readonly string _initialDir; private readonly Stack _remainingDirs = new Stack(); private readonly CommonProjectNode _project; public DiskMerger(CommonProjectNode project, HierarchyNode parent, string dir) { _project = project; _initialDir = dir; _remainingDirs.Push(new DirState(dir, parent)); } /// /// Continues processing the merge request, performing a portion of the full merge possibly /// returning before the merge has completed. /// /// Returns true if the merge needs to continue, or false if the merge has completed. /// public bool ContinueMerge() { if (_remainingDirs.Count == 0 || // all done !Directory.Exists(_initialDir)) { // directory went away return false; } var dir = _remainingDirs.Pop(); Debug.WriteLine(dir.Name); if (!Directory.Exists(dir.Name)) { return true; } HashSet missingChildren = new HashSet(dir.Parent.AllChildren); IEnumerable dirs; try { dirs = Directory.EnumerateDirectories(dir.Name); } catch { // directory was deleted, we don't have access, etc... return true; } foreach (var curDir in dirs) { if (_project.IsFileHidden(curDir)) { continue; } if (IsFileSymLink(curDir)) { if (IsRecursiveSymLink(_initialDir, curDir)) { // don't add recursive sym links continue; } // track symlinks, we won't get events on the directory _project.CreateSymLinkWatcher(curDir); } var existing = _project.AddAllFilesFolder(dir.Parent, curDir + Path.DirectorySeparatorChar); missingChildren.Remove(existing); _remainingDirs.Push(new DirState(curDir, existing)); } IEnumerable files; try { files = Directory.EnumerateFiles(dir.Name); } catch { // directory was deleted, we don't have access, etc... return true; } foreach (var file in files) { if (!Directory.Exists(dir.Name)) { // file went away break; } if (_project.IsFileHidden(file)) { continue; } missingChildren.Remove(_project.AddAllFilesFile(dir.Parent, file)); } // remove the excluded children which are no longer there foreach (var child in missingChildren) { if (child.ItemNode.IsExcluded) { _project.RemoveSubTree(child); } } return true; } class DirState { public readonly string Name; public readonly HierarchyNode Parent; public DirState(string name, HierarchyNode parent) { Name = name; Parent = parent; } } } private void MergeDiskNodes(HierarchyNode curParent, string dir) { var merger = new DiskMerger(this, curParent, dir); while (merger.ContinueMerge()) { } } private void RemoveSubTree(HierarchyNode node) { foreach (var child in node.AllChildren) { RemoveSubTree(child); } node.Parent.RemoveChild(node); _diskNodes.Remove(node.Url); } private static string GetFinalPathName(string dir) { using (var dirHandle = NativeMethods.CreateFile( dir, NativeMethods.FileDesiredAccess.FILE_LIST_DIRECTORY, NativeMethods.FileShareFlags.FILE_SHARE_DELETE | NativeMethods.FileShareFlags.FILE_SHARE_READ | NativeMethods.FileShareFlags.FILE_SHARE_WRITE, IntPtr.Zero, NativeMethods.FileCreationDisposition.OPEN_EXISTING, NativeMethods.FileFlagsAndAttributes.FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero )) { if (!dirHandle.IsInvalid) { uint pathLen = NativeMethods.MAX_PATH + 1; uint res; StringBuilder filePathBuilder; for (; ; ) { filePathBuilder = new StringBuilder(checked((int)pathLen)); res = NativeMethods.GetFinalPathNameByHandle( dirHandle, filePathBuilder, pathLen, 0 ); if (res != 0 && res < pathLen) { // we had enough space, and got the filename. break; } } if (res != 0) { Debug.Assert(filePathBuilder.ToString().StartsWith("\\\\?\\")); return filePathBuilder.ToString().Substring(4); } } } return dir; } private static bool IsRecursiveSymLink(string parentDir, string childDir) { if (IsFileSymLink(parentDir)) { // figure out the real parent dir so the check below works w/ multiple // symlinks pointing at each other parentDir = GetFinalPathName(parentDir); } string finalPath = GetFinalPathName(childDir); // check and see if we're recursing infinitely... if (CommonUtils.IsSubpathOf(finalPath, parentDir)) { // skip this file return true; } return false; } private static bool IsFileSymLink(string path) { try { return (File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0; } catch (UnauthorizedAccessException) { return false; } catch (DirectoryNotFoundException) { return false; } catch (FileNotFoundException) { return false; } } private bool IsFileHidden(string path) { if (String.Equals(path, FileName, StringComparison.OrdinalIgnoreCase) || String.Equals(path, FileName + ".user", StringComparison.OrdinalIgnoreCase)) { return true; } if(!File.Exists(path) && !Directory.Exists(path)) { // if the file has disappeared avoid the exception... return false; } try { return (File.GetAttributes(path) & (FileAttributes.Hidden | FileAttributes.System)) != 0; } catch (UnauthorizedAccessException) { return false; } catch (DirectoryNotFoundException) { return false; } catch (FileNotFoundException) { return false; } } /// /// Adds a file which is displayed when Show All Files is enabled /// private HierarchyNode AddAllFilesFile(HierarchyNode curParent, string file) { var existing = FindNodeByFullPath(file); if (existing == null) { var newFile = CreateFileNode(new AllFilesProjectElement(file, GetItemType(file), this)); AddAllFilesNode(curParent, newFile); return newFile; } return existing; } /// /// Adds a folder which is displayed when Show All files is enabled /// private HierarchyNode AddAllFilesFolder(HierarchyNode curParent, string curDir) { var folderNode = FindNodeByFullPath(curDir); if (folderNode == null) { folderNode = CreateFolderNode(new AllFilesProjectElement(curDir, "Folder", this)); AddAllFilesNode(curParent, folderNode); // Solution Explorer will expand the parent when an item is // added, which we don't want folderNode.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder); } return folderNode; } /// /// Initializes and adds a file or folder visible only when Show All files is enabled /// private void AddAllFilesNode(HierarchyNode parent, HierarchyNode newNode) { newNode.IsVisible = IsShowingAllFiles; newNode.ID = ItemIdMap.Add(newNode); parent.AddChild(newNode); } private void FileContentsChanged(object sender, FileSystemEventArgs e) { if (IsClosed) { return; } FileSystemEventHandler handler; if (_fileChangedHandlers.TryGetValue(e.FullPath, out handler)) { handler(sender, e); } } private void FileAttributesChanged(object sender, FileSystemEventArgs e) { lock (_fileSystemChanges) { if (NoPendingFileSystemRescan()) { _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Changed, e.FullPath)); TriggerIdle(); } } } private bool NoPendingFileSystemRescan() { return _fileSystemChanges.Count == 0 || _fileSystemChanges.Peek()._type != WatcherChangeTypes.All; } public void RegisterFileChangeNotification(FileNode node, FileSystemEventHandler handler) { _fileChangedHandlers[node.Url] = handler; } public void UnregisterFileChangeNotification(FileNode node) { _fileChangedHandlers.Remove(node.Url); } protected override ReferenceContainerNode CreateReferenceContainerNode() { return new CommonReferenceContainerNode(this); } private void FileNameChanged(object sender, RenamedEventArgs e) { if (IsClosed) { return; } lock (_fileSystemChanges) { // we just generate a delete and creation here - we're just updating the hierarchy // either changing icons or updating the non-project elements, so we don't need to // generate rename events or anything like that. This saves us from having to // handle updating the hierarchy in a special way for renames. if (NoPendingFileSystemRescan()) { _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Deleted, e.OldFullPath, true)); _fileSystemChanges.Enqueue(new FileSystemChange(this, WatcherChangeTypes.Created, e.FullPath, true)); TriggerIdle(); } } } private void FileExistanceChanged(object sender, FileSystemEventArgs e) { if (IsClosed) { return; } lock (_fileSystemChanges) { if (NoPendingFileSystemRescan()) { _fileSystemChanges.Enqueue(new FileSystemChange(this, e.ChangeType, e.FullPath)); TriggerIdle(); } } } /// /// If VS is already idle, we won't keep getting idle events, so we need to post a /// new event to the queue to flip away from idle and back again. /// private void TriggerIdle() { if (Interlocked.CompareExchange(ref _idleTriggered, 1, 0) == 0) { _uiSync.BeginInvoke(Nop, Type.EmptyTypes); } } private static readonly Action Nop = () => { }; public void CreateSymLinkWatcher(string curDir) { if (!CommonUtils.HasEndSeparator(curDir)) { curDir = curDir + Path.DirectorySeparatorChar; } _symlinkWatchers[curDir] = CreateFileSystemWatcher(curDir); } public bool TryDeactivateSymLinkWatcher(HierarchyNode child) { FileSystemWatcher watcher; if (_symlinkWatchers.TryGetValue(child.Url, out watcher)) { _symlinkWatchers.Remove(child.Url); watcher.EnableRaisingEvents = false; watcher.Dispose(); return true; } return false; } private void OnIdle(object sender, ComponentManagerEventArgs e) { Interlocked.Exchange(ref _idleTriggered, 0); do { using (new DebugTimer("ProcessFileChanges while Idle", 100)) { if (IsClosed) { return; } DiskMerger merger; FileSystemChange change = null; lock (_fileSystemChanges) { merger = _currentMerger; if (merger == null) { if (_fileSystemChanges.Count == 0) { break; } change = _fileSystemChanges.Dequeue(); } } if (merger != null) { // we have more file merges to process, do this // before reflecting any other pending updates... if (!merger.ContinueMerge()) { _currentMerger = null; } continue; } #if DEBUG try { #endif if (change._type == WatcherChangeTypes.All) { _currentMerger = new DiskMerger(this, this, ProjectHome); continue; } else { change.ProcessChange(); } #if DEBUG } catch (Exception ex) { Debug.Fail("Unexpected exception while processing change: {0}", ex.ToString()); throw; } #endif } } while (e.ComponentManager.FContinueIdle() != 0); } /// /// Represents an individual change to the file system. We process these in bulk on the /// UI thread. /// class FileSystemChange { private readonly CommonProjectNode _project; public readonly WatcherChangeTypes _type; private readonly string _path; private readonly bool _isRename; public FileSystemChange(CommonProjectNode node, WatcherChangeTypes changeType, string path, bool isRename = false) { _project = node; _type = changeType; _path = path; _isRename = isRename; } public override string ToString() { return "FileSystemChange: " + _isRename + " " + _type + " " + _path; } private void RedrawIcon(HierarchyNode node) { _project.ReDrawNode(node, UIHierarchyElement.Icon); for (var child = node.FirstChild; child != null; child = child.NextSibling) { RedrawIcon(child); } } public void ProcessChange() { var child = _project.FindNodeByFullPath(_path); if ((_type == WatcherChangeTypes.Deleted || _type == WatcherChangeTypes.Changed) && child == null) { child = _project.FindNodeByFullPath(_path + Path.DirectorySeparatorChar); } switch (_type) { case WatcherChangeTypes.Deleted: ChildDeleted(child); break; case WatcherChangeTypes.Created: ChildCreated(child); break; case WatcherChangeTypes.Changed: // we only care about the attributes if (_project.IsFileHidden(_path)) { if (child != null) { // attributes must of changed to hidden, remove the file goto case WatcherChangeTypes.Deleted; } } else { if (child == null) { // attributes must of changed from hidden, add the file goto case WatcherChangeTypes.Created; } } break; } } private void RemoveAllFilesChildren(HierarchyNode parent) { bool removed = false; for (var current = parent.FirstChild; current != null; current = current.NextSibling) { // remove our children first RemoveAllFilesChildren(current); _project.TryDeactivateSymLinkWatcher(current); // then remove us if we're an all files node if (current.ItemNode is AllFilesProjectElement) { parent.RemoveChild(current); _project.OnItemDeleted(current); removed = true; } } if (removed) { _project.OnInvalidateItems(parent); } } private void ChildDeleted(HierarchyNode child) { if (child != null) { _project.TryDeactivateSymLinkWatcher(child); if (child.ItemNode.IsExcluded) { RemoveAllFilesChildren(child); // deleting a show all files item, remove the node. child.Parent.RemoveChild(child); _project.OnItemDeleted(child); } else { Debug.Assert(!child.IsNonMemberItem); // deleting an item in the project, fix the icon, also // fix the icon of all children which we may have not // received delete notifications for RedrawIcon(child); } } } private void ChildCreated(HierarchyNode child) { if (child != null) { // creating an item which was in the project, fix the icon. _project.ReDrawNode(child, UIHierarchyElement.Icon); } else { if (_project.IsFileHidden(_path)) { // don't add hidden files/folders return; } // creating a new item, need to create the on all files node string parentDir = Path.GetDirectoryName(CommonUtils.TrimEndSeparator(_path)) + Path.DirectorySeparatorChar; HierarchyNode parent; if (CommonUtils.IsSamePath(parentDir, _project.ProjectHome)) { parent = _project; } else { parent = _project.FindNodeByFullPath(parentDir); } if (parent == null) { // we've hit an error while adding too many files, the file system watcher // couldn't keep up. That's alright, we'll merge the files in correctly // in a little while... return; } IVsUIHierarchyWindow uiHierarchy = UIHierarchyUtilities.GetUIHierarchyWindow(_project.Site, HierarchyNode.SolutionExplorer); uint curState; uiHierarchy.GetItemState(_project.GetOuterInterface(), parent.ID, (uint)__VSHIERARCHYITEMSTATE.HIS_Expanded, out curState ); if (Directory.Exists(_path)) { if (IsFileSymLink(_path)) { if (IsRecursiveSymLink(parentDir, _path)) { // don't add recusrive sym link directory return; } // otherwise we're going to need a new file system watcher _project.CreateSymLinkWatcher(_path); } var folderNode = _project.AddAllFilesFolder(parent, _path + Path.DirectorySeparatorChar); // we may have just moved a directory from another location (e.g. drag and // and drop in explorer), in which case we also need to process the items // which are in the folder that we won't receive create notifications for. if (_isRename) { // First, make sure we don't have any children RemoveAllFilesChildren(folderNode); // then add the folder nodes _project.MergeDiskNodes(folderNode, _path); } _project.OnInvalidateItems(folderNode); } else { _project.AddAllFilesFile(parent, _path); } if ((curState & (uint)__VSHIERARCHYITEMSTATE.HIS_Expanded) == 0) { // Solution Explorer will expand the parent when an item is // added, which we don't want, so we check it's state before // adding, and then collapse the folder if it was expanded. parent.ExpandItem(EXPANDFLAGS.EXPF_CollapseFolder); } } } } public override int GetGuidProperty(int propid, out Guid guid) { if ((__VSHPROPID)propid == __VSHPROPID.VSHPROPID_PreferredLanguageSID) { guid = new Guid("{EFB9A1D6-EA71-4F38-9BA7-368C33FCE8DC}");// GetLanguageServiceType().GUID; } else { return base.GetGuidProperty(propid, out guid); } return VSConstants.S_OK; } protected override bool IsItemTypeFileType(string type) { if (!base.IsItemTypeFileType(type)) { if (String.Compare(type, "Page", StringComparison.OrdinalIgnoreCase) == 0 || String.Compare(type, "ApplicationDefinition", StringComparison.OrdinalIgnoreCase) == 0 || String.Compare(type, "Resource", StringComparison.OrdinalIgnoreCase) == 0) { return true; } else { return false; } } else { //This is a well known item node type, so return true. return true; } } protected override NodeProperties CreatePropertiesObject() { return new CommonProjectNodeProperties(this); } public override int SetSite(Microsoft.VisualStudio.OLE.Interop.IServiceProvider site) { base.SetSite(site); //Initialize a new object to track project document changes so that we can update the StartupFile Property accordingly _projectDocListenerForStartupFileUpdates = new ProjectDocumentsListenerForStartupFileUpdates((ServiceProvider)Site, this); _projectDocListenerForStartupFileUpdates.Init(); return VSConstants.S_OK; } public override void Close() { if (null != _projectDocListenerForStartupFileUpdates) { _projectDocListenerForStartupFileUpdates.Dispose(); _projectDocListenerForStartupFileUpdates = null; } if (GetLibraryManagerType() != null) { LibraryManager libraryManager = ((IServiceContainer)Package).GetService(GetLibraryManagerType()) as LibraryManager; if (null != libraryManager) { libraryManager.UnregisterHierarchy(InteropSafeHierarchy); } } if (_watcher != null) { _watcher.EnableRaisingEvents = false; _watcher.Dispose(); _watcher = null; } if (_attributesWatcher != null) { _attributesWatcher.EnableRaisingEvents = false; _attributesWatcher.Dispose(); _attributesWatcher = null; } base.Close(); } public override void Load(string filename, string location, string name, uint flags, ref Guid iidProject, out int canceled) { base.Load(filename, location, name, flags, ref iidProject, out canceled); LibraryManager libraryManager = Site.GetService(GetLibraryManagerType()) as LibraryManager; if (null != libraryManager) { libraryManager.RegisterHierarchy(InteropSafeHierarchy); } //If this is a WPFFlavor-ed project, then add a project-level DesignerContext service to provide //event handler generation (EventBindingProvider) for the XAML designer. //this.OleServiceProvider.AddService(typeof(DesignerContext), new OleServiceProvider.ServiceCreatorCallback(this.CreateServices), false); } public void BoldStartupItem(HierarchyNode startupItem) { if (!_boldedStartupItem) { if (BoldItem(startupItem, true)) { _boldedStartupItem = true; } } } public bool BoldItem(HierarchyNode node, bool isBold) { IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow( Site as IServiceProvider, new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2; if (ErrorHandler.Succeeded(windows.SetItemAttribute( this.GetOuterInterface(), node.ID, (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold, isBold ))) { return true; } return false; } /// /// Overriding to provide project general property page /// /// protected override Guid[] GetConfigurationIndependentPropertyPages() { return new Guid[0]; } /// /// Create a file node based on an msbuild item. /// /// The msbuild item to be analyzed public override FileNode CreateFileNode(ProjectElement item) { VsUtilities.ArgumentNotNull("item", item); CommonFileNode newNode; if (IsCodeFile(item.GetFullPathForElement())) { newNode = CreateCodeFileNode(item); } else { newNode = CreateNonCodeFileNode(item); } string link = item.GetMetadata(ProjectFileConstants.Link); if (!String.IsNullOrWhiteSpace(link) || !CommonUtils.IsSubpathOf(ProjectHome, item.GetFullPathForElement())) { newNode.SetIsLinkFile(true); } string include = item.GetMetadata(ProjectFileConstants.Include); newNode.OleServiceProvider.AddService(typeof(EnvDTE.Project), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false); newNode.OleServiceProvider.AddService(typeof(EnvDTE.ProjectItem), newNode.ServiceCreator, false); /*if (!string.IsNullOrEmpty(include) && Path.GetExtension(include).Equals(".xaml", StringComparison.OrdinalIgnoreCase)) { //Create a DesignerContext for the XAML designer for this file newNode.OleServiceProvider.AddService(typeof(DesignerContext), newNode.ServiceCreator, false); }*/ newNode.OleServiceProvider.AddService(typeof(VSLangProj.VSProject), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false); return newNode; } /// /// Create a file node based on absolute file name. /// public override FileNode CreateFileNode(string absFileName) { // Avoid adding files to the project multiple times. Ultimately // we should not use project items and instead should have virtual items. string path = CommonUtils.GetRelativeFilePath(ProjectHome, absFileName); var prjItem = BuildProject.AddItem(GetItemType(path), path)[0]; return CreateFileNode(new MsBuildProjectElement(this, prjItem)); } public override FolderNode CreateFolderNode(ProjectElement element) { return new CommonFolderNode(this, element); } public string GetItemType(string filename) { if (IsCodeFile(filename)) { return "Compile"; } else { return "Content"; } } public ProjectElement MakeProjectElement(string type, string path) { var item = BuildProject.AddItem(type, path)[0]; return new MsBuildProjectElement(this, item); } public override int IsDirty(out int isDirty) { isDirty = 0; if (IsProjectFileDirty) { isDirty = 1; return VSConstants.S_OK; } isDirty = IsFlavorDirty(); return VSConstants.S_OK; } protected override void AddNewFileNodeToHierarchy(HierarchyNode parentNode, string fileName) { base.AddNewFileNodeToHierarchy(parentNode, fileName); SetProjectFileDirty(true); } public override DependentFileNode CreateDependentFileNode(MsBuildProjectElement item) { DependentFileNode node = base.CreateDependentFileNode(item); if (null != node) { string include = item.GetMetadata(ProjectFileConstants.Include); if (IsCodeFile(include)) { node.OleServiceProvider.AddService( typeof(SVSMDCodeDomProvider), new OleServiceProvider.ServiceCreatorCallback(CreateServices), false); } } return node; } /// /// Creates the format list for the open file dialog /// /// The formatlist to return /// Success public override int GetFormatList(out string formatlist) { formatlist = GetFormatList(); formatlist = formatlist.Replace('|', '\0') + '\0'; return VSConstants.S_OK; } protected override ConfigProvider CreateConfigProvider() { return new CommonConfigProvider(this); } #endregion #region Methods /// /// This method retrieves an instance of a service that /// allows to start a project or a file with or without debugging. /// public abstract IProjectLauncher/*!*/ GetLauncher(); /// /// Returns resolved value of the current working directory property. /// public string GetWorkingDirectory() { string workDir = GetProjectProperty(CommonConstants.WorkingDirectory, true); return CommonUtils.GetAbsoluteDirectoryPath(ProjectHome, workDir); } /// /// Returns resolved value of the startup file property. /// public string GetStartupFile() { string startupFile = GetProjectProperty(CommonConstants.StartupFile, true); if (string.IsNullOrEmpty(startupFile)) { //No startup file is assigned return null; } return CommonUtils.GetAbsoluteFilePath(ProjectHome, startupFile); } /// /// Whenever project property has changed - refresh project hierarachy. /// private void CommonProjectNode_OnProjectPropertyChanged(object sender, ProjectPropertyChangedArgs e) { switch (e.PropertyName) { case CommonConstants.StartupFile: RefreshStartupFile(this, CommonUtils.GetAbsoluteFilePath(ProjectHome, e.OldValue), CommonUtils.GetAbsoluteFilePath(ProjectHome, e.NewValue)); break; } } /// /// Same as VsShellUtilities.GetUIHierarchyWindow, but it doesn't contain a useless cast to IVsWindowPane /// which fails on Dev10 with the solution explorer window. /// private static IVsUIHierarchyWindow GetUIHierarchyWindow(IServiceProvider serviceProvider, Guid guidPersistenceSlot) { if (serviceProvider == null) { throw new ArgumentException("serviceProvider"); } IVsUIShell service = serviceProvider.GetService(typeof(SVsUIShell)) as IVsUIShell; if (service == null) { throw new InvalidOperationException(); } object pvar = null; IVsWindowFrame ppWindowFrame = null; IVsUIHierarchyWindow window = null; try { ErrorHandler.ThrowOnFailure(service.FindToolWindow(0, ref guidPersistenceSlot, out ppWindowFrame)); ErrorHandler.ThrowOnFailure(ppWindowFrame.GetProperty(-3001, out pvar)); } catch (COMException exception) { Trace.WriteLine("Exception :" + exception.Message); } finally { if (pvar != null) { window = (IVsUIHierarchyWindow)pvar; } } return window; } /// /// Returns first immediate child node (non-recursive) of a given type. /// private void RefreshStartupFile(HierarchyNode parent, string oldFile, string newFile) { IVsUIHierarchyWindow2 windows = GetUIHierarchyWindow( Site, new Guid(ToolWindowGuids80.SolutionExplorer)) as IVsUIHierarchyWindow2; for (HierarchyNode n = parent.FirstChild; n != null; n = n.NextSibling) { // TODO: Distinguish between real Urls and fake ones (eg. "References") if (windows != null) { var absUrl = CommonUtils.GetAbsoluteFilePath(parent.ProjectMgr.ProjectHome, n.Url); if (CommonUtils.IsSamePath(oldFile, absUrl)) { windows.SetItemAttribute( this, n.ID, (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold, false ); ReDrawNode(n, UIHierarchyElement.Icon); } else if (CommonUtils.IsSamePath(newFile, absUrl)) { windows.SetItemAttribute( this, n.ID, (uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold, true ); ReDrawNode(n, UIHierarchyElement.Icon); } } RefreshStartupFile(n, oldFile, newFile); } } /// /// Provide mapping from our browse objects and automation objects to our CATIDs /// private void InitializeCATIDs() { // The following properties classes are specific to current language so we can use their GUIDs directly AddCATIDMapping(typeof(OAProject), typeof(OAProject).GUID); // The following is not language specific and as such we need a separate GUID AddCATIDMapping(typeof(FolderNodeProperties), new Guid(CommonConstants.FolderNodePropertiesGuid)); // These ones we use the same as language file nodes since both refer to files AddCATIDMapping(typeof(FileNodeProperties), typeof(FileNodeProperties).GUID); AddCATIDMapping(typeof(IncludedFileNodeProperties), typeof(IncludedFileNodeProperties).GUID); // We could also provide CATIDs for references and the references container node, if we wanted to. } /// /// Parses SearchPath property into a list of distinct absolute paths, preserving the order. /// protected IList ParseSearchPath() { var searchPath = GetProjectProperty(CommonConstants.SearchPath, true); return ParseSearchPath(searchPath); } /// /// Parses SearchPath string into a list of distinct absolute paths, preserving the order. /// protected IList ParseSearchPath(string searchPath) { var result = new List(); if (!string.IsNullOrEmpty(searchPath)) { var seen = new HashSet(); foreach (var path in searchPath.Split(';')) { if (string.IsNullOrEmpty(path)) { continue; } var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path); if (seen.Add(absPath)) { result.Add(absPath); } } } return result; } /// /// Saves list of paths back as SearchPath project property. /// private void SaveSearchPath(IList value) { var valueStr = string.Join(";", value.Select(path => { var relPath = CommonUtils.GetRelativeFilePath(ProjectHome, path); if (string.IsNullOrEmpty(relPath)) { relPath = "."; } return relPath; })); SetProjectProperty(CommonConstants.SearchPath, valueStr); } /// /// Adds new search path to the SearchPath project property. /// public void AddSearchPathEntry(string newpath) { VsUtilities.ArgumentNotNull("newpath", newpath); IList searchPath = ParseSearchPath(); var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, newpath); if (searchPath.Contains(absPath, StringComparer.OrdinalIgnoreCase)) { return; } searchPath.Add(absPath); SaveSearchPath(searchPath); } /// /// Removes a given path from the SearchPath property. /// public void RemoveSearchPathEntry(string path) { IList searchPath = ParseSearchPath(); var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path); if (searchPath.Remove(path)) { SaveSearchPath(searchPath); } } /// /// Creates the services exposed by this project. /// private object CreateServices(Type serviceType) { object service = null; if (typeof(VSLangProj.VSProject) == serviceType) { service = VSProject; } else if (typeof(EnvDTE.Project) == serviceType) { service = GetAutomationObject(); } /*else if (typeof(DesignerContext) == serviceType) { service = this.DesignerContext; }*/ return service; } /// /// Executes Add Search Path menu command. /// public int AddSearchPath() { string dirName = BrowseForFolder( DynamicProjectSR.GetString(DynamicProjectSR.SelectFolderForSearchPath), ProjectHome); if (dirName != null) { AddSearchPathEntry(dirName); } return VSConstants.S_OK; } public string BrowseForFolder(string title, string initialDir) { // Get a reference to the UIShell. IVsUIShell uiShell = GetService(typeof(SVsUIShell)) as IVsUIShell; if (null == uiShell) { return null; } //Create a fill in a structure that defines Browse for folder dialog VSBROWSEINFOW[] browseInfo = new VSBROWSEINFOW[1]; //Dialog title browseInfo[0].pwzDlgTitle = title; //Initial directory - project directory browseInfo[0].pwzInitialDir = initialDir; //Parent window uiShell.GetDialogOwnerHwnd(out browseInfo[0].hwndOwner); //Max path length //This is WCHARS not bytes browseInfo[0].nMaxDirName = (uint)NativeMethods.MAX_PATH; //This struct size browseInfo[0].lStructSize = (uint)Marshal.SizeOf(typeof(VSBROWSEINFOW)); //Memory to write selected directory to. //Note: this one allocates unmanaged memory, which must be freed later // Add 1 for the null terminator and double since we are WCHARs not bytes IntPtr pDirName = Marshal.AllocCoTaskMem((NativeMethods.MAX_PATH + 1)*2); browseInfo[0].pwzDirName = pDirName; try { //Show the dialog int hr = uiShell.GetDirectoryViaBrowseDlg(browseInfo); if (hr == VSConstants.OLE_E_PROMPTSAVECANCELLED) { //User cancelled the dialog return null; } //Check for any failures ErrorHandler.ThrowOnFailure(hr); //Get selected directory return Marshal.PtrToStringAuto(browseInfo[0].pwzDirName); } finally { //Free allocated unmanaged memory if (pDirName != IntPtr.Zero) { Marshal.FreeCoTaskMem(pDirName); } } } #endregion #region IVsProjectSpecificEditorMap2 Members public int GetSpecificEditorProperty(string mkDocument, int propid, out object result) { // initialize output params result = null; //Validate input if (string.IsNullOrEmpty(mkDocument)) throw new ArgumentException("Was null or empty", "mkDocument"); // Make sure that the document moniker passed to us is part of this project // We also don't care if it is not a dynamic language file node uint itemid; int hr; if (ErrorHandler.Failed(hr = ParseCanonicalName(mkDocument, out itemid))) { return hr; } HierarchyNode hierNode = NodeFromItemId(itemid); if (hierNode == null || ((hierNode as CommonFileNode) == null)) return VSConstants.E_NOTIMPL; switch (propid) { case (int)__VSPSEPROPID.VSPSEPROPID_UseGlobalEditorByDefault: // don't show project default editor, every file supports Python. result = false; return VSConstants.E_FAIL; /*case (int)__VSPSEPROPID.VSPSEPROPID_ProjectDefaultEditorName: result = "Python Editor"; break;*/ } return VSConstants.S_OK; } public int GetSpecificEditorType(string mkDocument, out Guid guidEditorType) { // Ideally we should at this point initalize a File extension to EditorFactory guid Map e.g. // in the registry hive so that more editors can be added without changing this part of the // code. Dynamic languages only make usage of one Editor Factory and therefore we will return // that guid guidEditorType = GetEditorFactoryType().GUID; return VSConstants.S_OK; } public int GetSpecificLanguageService(string mkDocument, out Guid guidLanguageService) { guidLanguageService = Guid.Empty; return VSConstants.E_NOTIMPL; } public int SetSpecificEditorProperty(string mkDocument, int propid, object value) { return VSConstants.E_NOTIMPL; } #endregion #region IVsDeferredSaveProject Members /// /// Implements deferred save support. Enabled by unchecking Tools->Options->Solutions and Projects->Save New Projects Created. /// /// In this mode we save the project when the user selects Save All. We need to move all the files in the project /// over to the new location. /// public virtual int SaveProjectToLocation(string pszProjectFilename) { string oldName = Url; string basePath = CommonUtils.NormalizeDirectoryPath(Path.GetDirectoryName(this.FileName)); string newName = Path.GetDirectoryName(pszProjectFilename); IVsUIShell shell = this.Site.GetService(typeof(SVsUIShell)) as IVsUIShell; IVsSolution vsSolution = (IVsSolution)this.GetService(typeof(SVsSolution)); int canContinue; vsSolution.QueryRenameProject(this, FileName, pszProjectFilename, 0, out canContinue); if (canContinue == 0) { return VSConstants.OLE_E_PROMPTSAVECANCELLED; } _watcher.EnableRaisingEvents = false; _watcher.Dispose(); // we don't use RenameProjectFile because it sends the OnAfterRenameProject event too soon // and causes VS to think the solution has changed on disk. We need to send it after all // updates are complete. // save the new project to to disk SaveMSBuildProjectFileAs(pszProjectFilename); if (CommonUtils.IsSameDirectory(ProjectHome, basePath)) { // ProjectHome was set by SaveMSBuildProjectFileAs to point to the temporary directory. BuildProject.SetProperty(CommonConstants.ProjectHome, "."); // save the project again w/ updated file info BuildProjectLocationChanged(); // remove all the children, saving any dirty files, and collecting the list of open files MoveFilesForDeferredSave(this, basePath, newName); } else { // Project referenced external files only, so just update its location without moving // files around. BuildProjectLocationChanged(); } BuildProject.Save(); SetProjectFileDirty(false); // update VS that we've changed the project this.OnPropertyChanged(this, (int)__VSHPROPID.VSHPROPID_Caption, 0); // Update solution // Note we ignore the errors here because reporting them to the user isn't really helpful. // We've already completed all of the work to rename everything here. If OnAfterNameProject // fails for some reason then telling the user it failed is just confusing because all of // the work is done. And if someone wanted to prevent us from renaming the project file they // should have responded to QueryRenameProject. Likewise if we can't refresh the property browser // for any reason then that's not too interesting either - the users project has been saved to // the new location. // http://pytools.codeplex.com/workitem/489 vsSolution.OnAfterRenameProject((IVsProject)this, oldName, pszProjectFilename, 0); shell.RefreshPropertyBrowser(0); _watcher = CreateFileSystemWatcher(ProjectHome); _attributesWatcher = CreateAttributesWatcher(ProjectHome); return VSConstants.S_OK; } private void MoveFilesForDeferredSave(HierarchyNode node, string basePath, string baseNewPath) { if (node != null) { for (var child = node.FirstChild; child != null; child = child.NextSibling) { bool isOpen, isDirty, isOpenedByUs; uint docCookie; IVsPersistDocData persist; var docMgr = child.GetDocumentManager(); if (docMgr != null) { docMgr.GetDocInfo(out isOpen, out isDirty, out isOpenedByUs, out docCookie, out persist); int cancelled; if (isDirty) { child.ProjectMgr.SaveItem(VSSAVEFLAGS.VSSAVE_Save, null, docCookie, IntPtr.Zero, out cancelled); } } IDiskBasedNode diskNode = child as IDiskBasedNode; if (diskNode != null) { diskNode.RenameForDeferredSave(basePath, baseNewPath); } MoveFilesForDeferredSave(child, basePath, baseNewPath); } } } #endregion public void SuppressFileChangeNotifications() { _watcher.EnableRaisingEvents = false; _suppressFileWatcherCount++; } public void RestoreFileChangeNotifications() { if (--_suppressFileWatcherCount == 0) { _watcher.EnableRaisingEvents = true; } } #if DEV11_OR_LATER public override object GetProperty(int propId) { CommonFolderNode.BoldStartupOnIcon(propId, this); return base.GetProperty(propId); } #else public override int SetProperty(int propid, object value) { CommonFolderNode.BoldStartupOnExpand(propid, this); return base.SetProperty(propid, value); } #endif } }