1780 lines
72 KiB
C#
Raw Normal View History

/* ****************************************************************************
*
* 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<string, FileSystemEventHandler> _fileChangedHandlers = new Dictionary<string, FileSystemEventHandler>();
private Queue<FileSystemChange> _fileSystemChanges = new Queue<FileSystemChange>();
private object _fileSystemChangesLock = new object();
public UIThreadSynchronizer _uiSync;
private MSBuild.Project userBuildProject;
private readonly Dictionary<string, FileSystemWatcher> _symlinkWatchers = new Dictionary<string, FileSystemWatcher>();
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; }
}
/// <summary>
/// Get the VSProject corresponding to this project
/// </summary>
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;
}
}
/// <summary>
/// Indicates whether the project is currently is busy refreshing its hierarchy.
/// </summary>
public bool IsRefreshing {
get { return _isRefreshing; }
set { _isRefreshing = value; }
}
/// <summary>
/// Language specific project images
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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
/// </summary>
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<IProjectPublisher>();
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);
}
/// <summary>
/// As we don't register files/folders in the project file, removing an item is a noop.
/// </summary>
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<string> files, Action<MSBuildResult, string> uiThreadCallback) {
uiThreadCallback(MSBuildResult.Successful, target);
}
/// <summary>
/// Overriding main project loading method to inject our hierarachy of nodes.
/// </summary>
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;
}
/// <summary>
/// When the file system watcher buffer overflows we need to scan the entire
/// directory for changes.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
class DiskMerger {
private readonly string _initialDir;
private readonly Stack<DirState> _remainingDirs = new Stack<DirState>();
private readonly CommonProjectNode _project;
public DiskMerger(CommonProjectNode project, HierarchyNode parent, string dir) {
_project = project;
_initialDir = dir;
_remainingDirs.Push(new DirState(dir, parent));
}
/// <summary>
/// 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.
/// </summary>
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<HierarchyNode> missingChildren = new HashSet<HierarchyNode>(dir.Parent.AllChildren);
IEnumerable<string> 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<string> 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;
}
}
/// <summary>
/// Adds a file which is displayed when Show All Files is enabled
/// </summary>
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;
}
/// <summary>
/// Adds a folder which is displayed when Show All files is enabled
/// </summary>
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;
}
/// <summary>
/// Initializes and adds a file or folder visible only when Show All files is enabled
/// </summary>
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();
}
}
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Represents an individual change to the file system. We process these in bulk on the
/// UI thread.
/// </summary>
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<IVsUIHierarchy>(),
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<IVsUIHierarchy>(),
node.ID,
(uint)__VSHIERITEMATTRIBUTE.VSHIERITEMATTRIBUTE_Bold,
isBold
))) {
return true;
}
return false;
}
/// <summary>
/// Overriding to provide project general property page
/// </summary>
/// <returns></returns>
protected override Guid[] GetConfigurationIndependentPropertyPages() {
return new Guid[0];
}
/// <summary>
/// Create a file node based on an msbuild item.
/// </summary>
/// <param name="item">The msbuild item to be analyzed</param>
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;
}
/// <summary>
/// Create a file node based on absolute file name.
/// </summary>
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;
}
/// <summary>
/// Creates the format list for the open file dialog
/// </summary>
/// <param name="formatlist">The formatlist to return</param>
/// <returns>Success</returns>
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
/// <summary>
/// This method retrieves an instance of a service that
/// allows to start a project or a file with or without debugging.
/// </summary>
public abstract IProjectLauncher/*!*/ GetLauncher();
/// <summary>
/// Returns resolved value of the current working directory property.
/// </summary>
public string GetWorkingDirectory() {
string workDir = GetProjectProperty(CommonConstants.WorkingDirectory, true);
return CommonUtils.GetAbsoluteDirectoryPath(ProjectHome, workDir);
}
/// <summary>
/// Returns resolved value of the startup file property.
/// </summary>
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);
}
/// <summary>
/// Whenever project property has changed - refresh project hierarachy.
/// </summary>
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;
}
}
/// <summary>
/// Same as VsShellUtilities.GetUIHierarchyWindow, but it doesn't contain a useless cast to IVsWindowPane
/// which fails on Dev10 with the solution explorer window.
/// </summary>
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;
}
/// <summary>
/// Returns first immediate child node (non-recursive) of a given type.
/// </summary>
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);
}
}
/// <summary>
/// Provide mapping from our browse objects and automation objects to our CATIDs
/// </summary>
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.
}
/// <summary>
/// Parses SearchPath property into a list of distinct absolute paths, preserving the order.
/// </summary>
protected IList<string> ParseSearchPath() {
var searchPath = GetProjectProperty(CommonConstants.SearchPath, true);
return ParseSearchPath(searchPath);
}
/// <summary>
/// Parses SearchPath string into a list of distinct absolute paths, preserving the order.
/// </summary>
protected IList<string> ParseSearchPath(string searchPath) {
var result = new List<string>();
if (!string.IsNullOrEmpty(searchPath)) {
var seen = new HashSet<string>();
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;
}
/// <summary>
/// Saves list of paths back as SearchPath project property.
/// </summary>
private void SaveSearchPath(IList<string> 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);
}
/// <summary>
/// Adds new search path to the SearchPath project property.
/// </summary>
public void AddSearchPathEntry(string newpath) {
VsUtilities.ArgumentNotNull("newpath", newpath);
IList<string> searchPath = ParseSearchPath();
var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, newpath);
if (searchPath.Contains(absPath, StringComparer.OrdinalIgnoreCase)) {
return;
}
searchPath.Add(absPath);
SaveSearchPath(searchPath);
}
/// <summary>
/// Removes a given path from the SearchPath property.
/// </summary>
public void RemoveSearchPathEntry(string path) {
IList<string> searchPath = ParseSearchPath();
var absPath = CommonUtils.GetAbsoluteFilePath(ProjectHome, path);
if (searchPath.Remove(path)) {
SaveSearchPath(searchPath);
}
}
/// <summary>
/// Creates the services exposed by this project.
/// </summary>
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;
}
/// <summary>
/// Executes Add Search Path menu command.
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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
}
}