/* **************************************************************************** * * 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.Contracts; using System.Runtime.InteropServices; using Microsoft.VisualStudio; using Microsoft.VisualStudio.ComponentModelHost; using Microsoft.VisualStudio.Editor; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Shell.Interop; using Microsoft.VisualStudio.Text; using Microsoft.VisualStudio.TextManager.Interop; using Microsoft.VisualStudio.Project; namespace Microsoft.VisualStudio.Navigation { /// /// Inplementation of the service that builds the information to expose to the symbols /// navigation tools (class view or object browser) from the source files inside a /// hierarchy. /// public abstract partial class LibraryManager : IDisposable, IVsRunningDocTableEvents { private readonly CommonPackage/*!*/ _package; private readonly Dictionary _documents; private readonly Dictionary _hierarchies; private readonly Dictionary _files; private readonly Library _library; private readonly IVsEditorAdaptersFactoryService _adapterFactory; private uint _objectManagerCookie; private uint _runningDocTableCookie; public LibraryManager(CommonPackage/*!*/ package) { Contract.Assert(package != null); _package = package; _documents = new Dictionary(); _hierarchies = new Dictionary(); _library = new Library(new Guid(CommonConstants.LibraryGuid)); _library.LibraryCapabilities = (_LIB_FLAGS2)_LIB_FLAGS.LF_PROJECT; _files = new Dictionary(); var model = ((IServiceContainer)package).GetService(typeof(SComponentModel)) as IComponentModel; _adapterFactory = model.GetService(); // Register our library now so it'll be available for find all references RegisterLibrary(); } public Library Library { get { return _library; } } protected abstract LibraryNode CreateLibraryNode(IScopeNode subItem, string namePrefix, IVsHierarchy hierarchy, uint itemid); public virtual LibraryNode CreateFileLibraryNode(HierarchyNode hierarchy, string name, string filename, LibraryNodeType libraryNodeType) { return new LibraryNode(name, filename, libraryNodeType); } private object GetPackageService(Type/*!*/ type) { return ((System.IServiceProvider)_package).GetService(type); } private void RegisterForRDTEvents() { if (0 != _runningDocTableCookie) { return; } IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; if (null != rdt) { // Do not throw here in case of error, simply skip the registration. rdt.AdviseRunningDocTableEvents(this, out _runningDocTableCookie); } } private void UnregisterRDTEvents() { if (0 == _runningDocTableCookie) { return; } IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; if (null != rdt) { // Do not throw in case of error. rdt.UnadviseRunningDocTableEvents(_runningDocTableCookie); } _runningDocTableCookie = 0; } #region ILibraryManager Members public void RegisterHierarchy(IVsHierarchy hierarchy) { if ((null == hierarchy) || _hierarchies.ContainsKey(hierarchy)) { return; } RegisterLibrary(); HierarchyListener listener = new HierarchyListener(hierarchy, this); listener.StartListening(true); _hierarchies.Add(hierarchy, listener); RegisterForRDTEvents(); } private void RegisterLibrary() { if (0 == _objectManagerCookie) { IVsObjectManager2 objManager = GetPackageService(typeof(SVsObjectManager)) as IVsObjectManager2; if (null == objManager) { return; } Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure( objManager.RegisterSimpleLibrary(_library, out _objectManagerCookie)); } } public void UnregisterHierarchy(IVsHierarchy hierarchy) { if ((null == hierarchy) || !_hierarchies.ContainsKey(hierarchy)) { return; } HierarchyListener listener = _hierarchies[hierarchy]; if (null != listener) { listener.Dispose(); } _hierarchies.Remove(hierarchy); if (0 == _hierarchies.Count) { UnregisterRDTEvents(); } lock (_files) { ModuleId[] keys = new ModuleId[_files.Keys.Count]; _files.Keys.CopyTo(keys, 0); foreach (ModuleId id in keys) { if (hierarchy.Equals(id.Hierarchy)) { _library.RemoveNode(_files[id]); _files.Remove(id); } } } // Remove the document listeners. uint[] docKeys = new uint[_documents.Keys.Count]; _documents.Keys.CopyTo(docKeys, 0); foreach (uint id in docKeys) { TextLineEventListener docListener = _documents[id]; if (hierarchy.Equals(docListener.FileID.Hierarchy)) { _documents.Remove(id); docListener.Dispose(); } } } public void RegisterLineChangeHandler(uint document, TextLineChangeEvent lineChanged, Action onIdle) { _documents[document].OnFileChangedImmediate += delegate(object sender, TextLineChange[] changes, int fLast) { lineChanged(sender, changes, fLast); }; _documents[document].OnFileChanged += (sender, args) => onIdle(args.TextBuffer); } #endregion #region Library Member Production /// /// Overridden in the base class to receive notifications of when a file should /// be analyzed for inclusion in the library. The derived class should queue /// the parsing of the file and when it's complete it should call FileParsed /// with the provided LibraryTask and an IScopeNode which provides information /// about the members of the file. /// protected virtual void OnNewFile(LibraryTask task) { } /// /// Called by derived class when a file has been parsed. The caller should /// provide the LibraryTask received from the OnNewFile call and an IScopeNode /// which represents the contents of the library. /// /// It is safe to call this method from any thread. /// protected void FileParsed(LibraryTask task, IScopeNode scope) { try { var project = task.ModuleID.Hierarchy.GetProject().GetCommonProject(); HierarchyNode fileNode = fileNode = project.NodeFromItemId(task.ModuleID.ItemID); if (fileNode == null) { return; } LibraryNode module = CreateFileLibraryNode( fileNode, System.IO.Path.GetFileName(task.FileName), task.FileName, LibraryNodeType.PhysicalContainer ); // TODO: Creating the module tree should be done lazily as needed // Currently we replace the entire tree and rely upon the libraries // update count to invalidate the whole thing. We could do this // finer grained and only update the changed nodes. But then we // need to make sure we're not mutating lists which are handed out. CreateModuleTree(module, module, scope, task.FileName + ":", task.ModuleID); if (null != task.ModuleID) { LibraryNode previousItem = null; lock (_files) { if (_files.TryGetValue(task.ModuleID, out previousItem)) { _files.Remove(task.ModuleID); } } _library.RemoveNode(previousItem); } _library.AddNode(module); if (null != task.ModuleID) { lock (_files) { _files.Add(task.ModuleID, module); } } } catch (COMException) { // we're shutting down and can't get the project } } private void CreateModuleTree(LibraryNode root, LibraryNode current, IScopeNode scope, string namePrefix, ModuleId moduleId) { if ((null == root) || (null == scope) || (null == scope.NestedScopes)) { return; } foreach (IScopeNode subItem in scope.NestedScopes) { LibraryNode newNode = CreateLibraryNode(subItem, namePrefix, moduleId.Hierarchy, moduleId.ItemID); string newNamePrefix = namePrefix; // The classes are always added to the root node, the functions to the // current node. if ((newNode.NodeType & LibraryNodeType.Classes) != LibraryNodeType.None) { // Classes are always added to the root. root.AddNode(newNode); newNamePrefix = namePrefix + newNode.Name + "."; } else { current.AddNode(newNode); } // Now use recursion to get the other types. CreateModuleTree(root, newNode, subItem, newNamePrefix, moduleId); } } #endregion #region Hierarchy Events private void OnNewFile(object sender, HierarchyEventArgs args) { IVsHierarchy hierarchy = sender as IVsHierarchy; if (null == hierarchy || IsNonMemberItem(hierarchy, args.ItemID)) { return; } ITextBuffer buffer = null; if (null != args.TextBuffer) { buffer = _adapterFactory.GetDocumentBuffer(args.TextBuffer); } var id = new ModuleId(hierarchy, args.ItemID); OnNewFile(new LibraryTask(args.CanonicalName, buffer, new ModuleId(hierarchy, args.ItemID))); } /// /// Handles the delete event, checking to see if this is a project item. /// /// /// private void OnDeleteFile(object sender, HierarchyEventArgs args) { IVsHierarchy hierarchy = sender as IVsHierarchy; if (null == hierarchy || IsNonMemberItem(hierarchy, args.ItemID)) { return; } OnDeleteFile(hierarchy, args); } /// /// Does a delete w/o checking if it's a non-meber item, for handling the /// transition from member item to non-member item. /// private void OnDeleteFile(IVsHierarchy hierarchy, HierarchyEventArgs args) { ModuleId id = new ModuleId(hierarchy, args.ItemID); LibraryNode node = null; lock (_files) { if (_files.TryGetValue(id, out node)) { _files.Remove(id); } } if (null != node) { _library.RemoveNode(node); } } private void IsNonMemberItemChanged(object sender, HierarchyEventArgs args) { IVsHierarchy hierarchy = sender as IVsHierarchy; if (null == hierarchy) { return; } if (!IsNonMemberItem(hierarchy, args.ItemID)) { OnNewFile(hierarchy, args); } else { OnDeleteFile(hierarchy, args); } } /// /// Checks whether this hierarchy item is a project member (on disk items from show all /// files aren't considered project members). /// protected bool IsNonMemberItem(IVsHierarchy hierarchy, uint itemId) { object val; int hr = hierarchy.GetProperty(itemId, (int)__VSHPROPID.VSHPROPID_IsNonMemberItem, out val); return ErrorHandler.Succeeded(hr) && (bool)val; } #endregion #region IVsRunningDocTableEvents Members public int OnAfterAttributeChange(uint docCookie, uint grfAttribs) { if ((grfAttribs & (uint)(__VSRDTATTRIB.RDTA_MkDocument)) == (uint)__VSRDTATTRIB.RDTA_MkDocument) { IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; if (rdt != null) { uint flags, readLocks, editLocks, itemid; IVsHierarchy hier; IntPtr docData = IntPtr.Zero; string moniker; int hr; try { hr = rdt.GetDocumentInfo(docCookie, out flags, out readLocks, out editLocks, out moniker, out hier, out itemid, out docData); TextLineEventListener listner; if (_documents.TryGetValue(docCookie, out listner)) { listner.FileName = moniker; } } finally { if (IntPtr.Zero != docData) { Marshal.Release(docData); } } } } return VSConstants.S_OK; } public int OnAfterDocumentWindowHide(uint docCookie, IVsWindowFrame pFrame) { return VSConstants.S_OK; } public int OnAfterFirstDocumentLock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) { return VSConstants.S_OK; } public int OnAfterSave(uint docCookie) { return VSConstants.S_OK; } public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame) { // Check if this document is in the list of the documents. if (_documents.ContainsKey(docCookie)) { return VSConstants.S_OK; } // Get the information about this document from the RDT. IVsRunningDocumentTable rdt = GetPackageService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable; if (null != rdt) { // Note that here we don't want to throw in case of error. uint flags; uint readLocks; uint writeLoks; string documentMoniker; IVsHierarchy hierarchy; uint itemId; IntPtr unkDocData; int hr = rdt.GetDocumentInfo(docCookie, out flags, out readLocks, out writeLoks, out documentMoniker, out hierarchy, out itemId, out unkDocData); try { if (Microsoft.VisualStudio.ErrorHandler.Failed(hr) || (IntPtr.Zero == unkDocData)) { return VSConstants.S_OK; } // Check if the herarchy is one of the hierarchies this service is monitoring. if (!_hierarchies.ContainsKey(hierarchy)) { // This hierarchy is not monitored, we can exit now. return VSConstants.S_OK; } // Check the file to see if a listener is required. if (_package.IsRecognizedFile(documentMoniker)) { return VSConstants.S_OK; } // Create the module id for this document. ModuleId docId = new ModuleId(hierarchy, itemId); // Try to get the text buffer. IVsTextLines buffer = Marshal.GetObjectForIUnknown(unkDocData) as IVsTextLines; // Create the listener. TextLineEventListener listener = new TextLineEventListener(buffer, documentMoniker, docId); // Set the event handler for the change event. Note that there is no difference // between the AddFile and FileChanged operation, so we can use the same handler. listener.OnFileChanged += new EventHandler(OnNewFile); // Add the listener to the dictionary, so we will not create it anymore. _documents.Add(docCookie, listener); } finally { if (IntPtr.Zero != unkDocData) { Marshal.Release(unkDocData); } } } // Always return success. return VSConstants.S_OK; } public int OnBeforeLastDocumentUnlock(uint docCookie, uint dwRDTLockType, uint dwReadLocksRemaining, uint dwEditLocksRemaining) { if ((0 != dwEditLocksRemaining) || (0 != dwReadLocksRemaining)) { return VSConstants.S_OK; } TextLineEventListener listener; if (!_documents.TryGetValue(docCookie, out listener) || (null == listener)) { return VSConstants.S_OK; } using (listener) { _documents.Remove(docCookie); // Now make sure that the information about this file are up to date (e.g. it is // possible that Class View shows something strange if the file was closed without // saving the changes). HierarchyEventArgs args = new HierarchyEventArgs(listener.FileID.ItemID, listener.FileName); OnNewFile(listener.FileID.Hierarchy, args); } return VSConstants.S_OK; } #endregion public void OnIdle(IOleComponentManager compMgr) { foreach (TextLineEventListener listener in _documents.Values) { if (compMgr.FContinueIdle() == 0) { break; } listener.OnIdle(); } } #region IDisposable Members public void Dispose() { // Dispose all the listeners. foreach (HierarchyListener listener in _hierarchies.Values) { listener.Dispose(); } _hierarchies.Clear(); foreach (TextLineEventListener textListener in _documents.Values) { textListener.Dispose(); } _documents.Clear(); // Remove this library from the object manager. if (0 != _objectManagerCookie) { IVsObjectManager2 mgr = GetPackageService(typeof(SVsObjectManager)) as IVsObjectManager2; if (null != mgr) { mgr.UnregisterLibrary(_objectManagerCookie); } _objectManagerCookie = 0; } // Unregister this object from the RDT events. UnregisterRDTEvents(); } #endregion } }