Visual Studio Extension: Project context is applied to every opened file except the first one

135 views Asked by At

I have a Visual Studio extension package where I am applying C++ syntax settings to custom file extensions. This is done in the Visual Studio's Text Editor options. Those files are plain text and I mean to have them behave as code files in the IDE (IntelliSense, find matching braces, etc...)

It's mostly working fine, but there is one problem. The C++ syntax context is not applied to whichever is the first file I open in a given Visual Studio session. I will launch Visual Studio, open one of our custom projects, and open one file. The IDE opens a document window and the file is opened, can be edited and saved, no problem in appearance. But the file behaves as a plain text and not a C++ source. Now, whenever I open a second file in the IDE, or any further file, the C++ settings do get applied successfully. I can close all document tabs, and open new ones, and all those tabs are fine. Even re-opening the first file in a new tab, or after re-loading the project or the solution, is fine. Only the first document opened in a Visual Studio session has the issue.

For the following segment, I will refer to the Microsoft documentation on using their standard editor: https ://msdn.microsoft.com/en-us/library/bb166504.aspx

To implement the OpenItem method with a standard editor

1.Call IVsRunningDocumentTable (RDT_EditLock) to determine whether the document data object file is already open.

2.If the file is already open, resurface the file by calling the IsDocumentOpen method, specifying a value of IDO_ActivateIfOpen for the grfIDO parameter.

If the file is open and the document is owned by a different project than the calling project, your project receives a warning that the editor being opened is from another project. The file window is then surfaced.

3.If the document is not open or not in the running document table, call the OpenStandardEditor method (OSE_ChooseBestStdEditor) to open a standard editor for the file.

When you call the method, the IDE performs the following tasks:

a.The IDE scans the Editors/{guidEditorType}/Extensions subkey in the registry to determine which editor can open the file and has the highest priority for doing this.

b.After the IDE has determined which editor can open the file, the IDE calls CreateEditorInstance. The editor's implementation of this method returns information that is required for the IDE to call CreateDocumentWindow and site the newly opened document.

c.Finally, the IDE loads the document by using the usual persistence interface, such as IVsPersistDocData2.

d.If the IDE has previously determined that the hierarchy or hierarchy item is available, the IDE calls GetItemContext method on the project to get a project-level context IServiceProvider pointer to pass back in with the CreateDocumentWindow method call.

4.Return an IServiceProvider pointer to the IDE when the IDE calls GetItemContext on your project if you want to let the editor get context from your project.

Performing this step lets the project offer additional services to the editor.

If the document view or document view object was successfully sited in a window frame, the object is initialized with its data by calling LoadDocData.

It definitely seems to me that I need to hit element (D) from the above instructions. I have debuged through my extension code, and I do see where my implementation of GetItemContext() comes into play. When I open most files, the code path does effectively go through this method, however it does not when I open the first file of a Visual Studio session.

Call stack from OpenStandardEditor

GetItemContext is invoked by the Microsoft assemblies and I do not know what is the condition that triggers whether it is called or not. I can only trace up to my call to the method OpenStandardEditor(), in FileDocumentManager.cs, then I don't know what happens beyond that. The above screenshot is the call stack when GetItemContext is successfully invoked, but when I'm opening the first file I'm totally in the dark as to what OpenStandardEditor is doing. I do know that in both cases, when the context is loaded and when it is not, the exact same parameter values are passed to OpenStandardEditor. So here's my code where this method is invoked, if that can be of some help:

My override of class DocumentManager:
    private int Open(bool newFile, bool openWith, uint editorFlags, ref Guid editorType, string physicalView, ref Guid logicalView, IntPtr docDataExisting, out IVsWindowFrame windowFrame, WindowFrameShowAction windowFrameAction)
    {
        windowFrame = null;
        if (this.Node == null || this.Node.ProjectMgr == null || this.Node.ProjectMgr.IsClosed)
        {
            return VSConstants.E_FAIL;
        }

        int returnValue = VSConstants.S_OK;
        string caption = this.GetOwnerCaption();
        string fullPath = this.GetFullPathForDocument();

        // Make sure that the file is on disk before we open the editor and display message if not found
        if (!((FileNode)this.Node).IsFileOnDisk(true))
        {
            // Inform clients that we have an invalid item (wrong icon)
            this.Node.OnInvalidateItems(this.Node.Parent);

            // Bail since we are not able to open the item
            return VSConstants.E_FAIL;
        }

        IVsUIShellOpenDocument uiShellOpenDocument = this.Node.ProjectMgr.Site.GetService(typeof(SVsUIShellOpenDocument)) as IVsUIShellOpenDocument;
        IOleServiceProvider serviceProvider = this.Node.ProjectMgr.Site.GetService(typeof(IOleServiceProvider)) as IOleServiceProvider;

        try
        {
            int result = VSConstants.E_FAIL;

            if (openWith)
            {
                result = uiShellOpenDocument.OpenStandardEditor((uint)__VSOSEFLAGS.OSE_UseOpenWithDialog, fullPath, ref logicalView, caption, this.Node.ProjectMgr, this.Node.ID, docDataExisting, serviceProvider, out windowFrame);
            }
            else
            {
                __VSOSEFLAGS openFlags = 0;
                if (newFile)
                {
                    openFlags |= __VSOSEFLAGS.OSE_OpenAsNewFile;
                }                   

                //NOTE: we MUST pass the IVsProject in pVsUIHierarchy and the itemid
                // of the node being opened, otherwise the debugger doesn't work.
                if (editorType != Guid.Empty)
                {
                    result = uiShellOpenDocument.OpenSpecificEditor(editorFlags, fullPath, ref editorType, physicalView, ref logicalView, caption, this.Node.ProjectMgr, this.Node.ID, docDataExisting, serviceProvider, out windowFrame);
                }
                else
                {
                    openFlags |= __VSOSEFLAGS.OSE_ChooseBestStdEditor;

// THIS IS THE CALL THAT I'M ALWAYS INVOKING.  PARAMS ARE ALWAYS THE SAME, BUT ITEM CONTEXT IS NOT ACTIVATED FOR FIRST FILE OF A SESSION.

                    result = uiShellOpenDocument.OpenStandardEditor((uint)openFlags, fullPath, ref logicalView, caption, this.Node.ProjectMgr, this.Node.ID, docDataExisting, serviceProvider, out windowFrame);
                }
            }

            if (result != VSConstants.S_OK && result != VSConstants.S_FALSE && result != VSConstants.OLE_E_PROMPTSAVECANCELLED)
            {
                ErrorHandler.ThrowOnFailure(result);
            }

            if (windowFrame != null)
            {
                object var;

                if (newFile)
                {
                    ErrorHandler.ThrowOnFailure(windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocData, out var));
                    IVsPersistDocData persistDocData = (IVsPersistDocData)var;
                    ErrorHandler.ThrowOnFailure(persistDocData.SetUntitledDocPath(fullPath));
                }

                var = null;
                ErrorHandler.ThrowOnFailure(windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocCookie, out var));
                this.Node.DocCookie = (uint)(int)var;

                if (windowFrameAction == WindowFrameShowAction.Show)
                {
                    ErrorHandler.ThrowOnFailure(windowFrame.Show());
                }
                else if (windowFrameAction == WindowFrameShowAction.ShowNoActivate)
                {
                    ErrorHandler.ThrowOnFailure(windowFrame.ShowNoActivate());
                }
                else if (windowFrameAction == WindowFrameShowAction.Hide)
                {
                    ErrorHandler.ThrowOnFailure(windowFrame.Hide());
                }
            }
        }
        catch (COMException e)
        {
            Trace.WriteLine("Exception e:" + e.Message);
            returnValue = e.ErrorCode;
            this.CloseWindowFrame(ref windowFrame);
        }

        return returnValue;
    }

I have also tried an alternative. In the call stack where I perform DoDefaultAction on my FileNode (extends HierarchyNode), I normally call an instance of my DocumentManager.Open() directly. I have changed that to try OpenDocumentViaProject() instead. Now, the MSENV assembly turns out to call my GetItemContext, then goes out to my implementation of DocumentManager.Open I quoted above.

Call stack from OpenDocumentViaProject

Sounds promising... but no. Beyond the screenshot above, once I call OpenStandardEditor the exact same behavior happens. No project context is applied to the first document opened in a session, and the context is applied to every further file. The call to GetItemContext() that is done by OpenDocumentViaProject() does not seem to matter in the slightest. Only when OpenStandardEditor() also ends up calling GetItemContext() somewhere downstream does the project settings I want get applied.

I don't see where I would be doing something fundamentally wrong. It seems to me that I am following the Mimcrosoft instructions on opening standard editors. Would you have a clue as to how my GetItemContext implementation is not invoked when I'm opening the first file of a VS session? Thanks

0

There are 0 answers