Search code examples
visual-studiovisual-studio-extensionsvsix

How to tell if the active document is a text document?


I'm developing a Visual Studio extension in which one of the implemented commands needs to be available only when the active document is a text document (like e.g. the Visual Studio's "Toggle Bookmark" does). The problem is that I can't figure out how to tell when that's the case.

Right now I have a half working solution. In the package's Initialize method I subscribe to DTE's WindowActivated event, and then whenever a window is activated I check if the window DocumentData property is of type TextDocument:

protected override void Initialize()
{
    base.Initialize();

    var dte = Package.GetGlobalService(typeof(EnvDTE.DTE)) as EnvDTE.DTE;
    dte.Events.WindowEvents.WindowActivated += WindowEventsOnWindowActivated;

    //More initialization here...
}

//This is checked from command's BeforeQueryStatus
public bool ActiveDocumentIsText { get; private set; } = false;

private void WindowEventsOnWindowActivated(Window gotFocus, Window lostFocus)
{
    if (gotFocus.Kind != "Document")                
        return; //It's not a document (e.g. it's a tool window)

    TextDocument textDoc = gotFocus.DocumentData as TextDocument;
    ActiveDocumentIsText = textDoc != null;
}

The problem with this approach is that 1) Window.DocumentData is documented as ".NET Framework internal use only", and 2) this gives a false positive when a document that has both a code view and a design view (e.g. a .visxmanifest file) is open in design mode.

I have tried to use IVsTextManager.GetActiveView as well, but this is returning the last active text view opened - so if I open a .txt file and then a .png file, it returns data for the .txt file even if it's not the active document anymore.

So, how do I check if the active document is a text document, or the code view of a document that can have a designer... and if possible, not using "undocumented" classes/members?

UPDATE: I found a slightly better solution. Inside the window activated handler:

ActiveDocumentIsText = gotFocus.Document.Object("TextDocument") != null;

At least this one is properly documented, but I still have the problem of false positives with designers.


Solution

  • I finally got it. It's somewhat tricky, but it works and is 100% "legal". Here's the recipe:

    1- Make the package class implement IVsRunningDocTableEvents. Make all the methods just return VSConstants.S_OK;

    2- Add the following field and the following auxiliary method to the package class:

    private IVsRunningDocumentTable runningDocumentTable;
    
    private bool DocIsOpenInLogicalView(string path, Guid logicalView, out IVsWindowFrame windowFrame)
    {
        return VsShellUtilities.IsDocumentOpen(
            this,
            path,
            VSConstants.LOGVIEWID_TextView,
            out var dummyHierarchy2, out var dummyItemId2,
            out windowFrame);
    }
    

    3- Add the following to the Initialize method of the package class:

    runningDocumentTable = GetService(typeof(SVsRunningDocumentTable)) as IVsRunningDocumentTable;
    runningDocumentTable.AdviseRunningDocTableEvents(this, out var dummyCookie);
    

    4- Don't blink, here comes the magic! Implement the IVsRunningDocTableEvents.OnBeforeDocumentWindowShow method as follows:

    public int OnBeforeDocumentWindowShow(uint docCookie, int fFirstShow, IVsWindowFrame pFrame)
    {
        runningDocumentTable.GetDocumentInfo(docCookie,
            out var dummyFlags, out var dummyReadLocks, out var dummyEditLocks,
            out string path, 
            out var dummyHierarchy, out var dummyItemId, out var dummyData);
    
        IVsWindowFrame windowFrameForTextView;
        var docIsOpenInTextView =
            DocIsOpenInLogicalView(path, VSConstants.LOGVIEWID_Code, out windowFrameForTextView) ||
            DocIsOpenInLogicalView(path, VSConstants.LOGVIEWID_TextView, out windowFrameForTextView);
    
        //Is the document open in the code/text view,
        //AND the window for that view is the one that has been just activated?
    
        ActiveDocumentIsText = docIsOpenInTextView && pFrame == logicalViewWindowFrame;
    
        return VSConstants.S_OK;
    }