Getting information out of Visual Studio

As I write this (on 14.Nov.2024) I'm developing a small Visual Studio Add In that I hope to put in the marketplace soon.

If you ever wrote an extension to Visual Studio, you know that it's a small miracle that the thing works as good as it does. Internally (or at least in the available automation API), that freaking thing is overengineered to a fault.

I haven't counted yet, but it feels that Visual Studio has about thousands of interfaces, working in 50 different layers using at least 10 different technologies.

Just an example, Visual Studio 2022:

  • The interface runs in Windows Presentation Foundation.
  • Running, internally, as a managed application.
  • With COM objects to communicate internally.
  • On top of Win32 forms.

Be that as it may, it is the IDE we have and use.

I want my add in to work in a language agnostic way, that is, it should work regardless of language you're using, as long as you have a text editor active.

If you used Visual Studio at all, you know that at the top of a text editor there are, depending on the file opened, breadcrumb combo boxes, as shown in the image below:

A Python file opened in a Visual Studio code editor.

The file in the image is a simple Python file opened outside a project. You can clearly see two combo boxes (or more accurately, a drop-down list) showing the class and the method where the cursor is at.

Generally, Visual Studio does this through an implementation of CodeModel

However, that's not always the case.

The file above clearly has the breadcrumb combos visible in the editor, but the FileCodeModel property of the active document (obtained through DTE.ActiveDocument.ProjectItem.FileCodeModel is, weirdly enough, null.

So, how does one can get the method/class at the cursor?

Well, you query the active window itself... If you can figure out which window to query.

You see, if you try to get the window through the obvious DTE.ActiveDocument.ActiveWindow, you'll get the Window interface and you need the IVsCodeWindow.

To get this one you need to start from a IVsMonitorSelection, to GetCurrentElementValue, which gives IVsWindowFrame, to finally GetProperty whose numerical id is “__VSFPROPID.VSFPROPID_DocView (-3001)”.

Only then, the result of the GetProperty is the active IVsCodeWindow, which is the type of window you want.

An object that implements IVsCodeWindow also should implement IVsDropBarManager, so you can do a simple cast.

Finally, with an IVsDropBarManager, you can GetDropDownBar, from which you can GetCurrentSelection, which is the index in the drop-down list. To get the text you need to GetClient of the drop-down bar to, at last, GetText.

One little catch, though.

The GetText function expects the type of the bar, which is an int: 0 gets you the first drop down (generally the class, but it can be the project), 1 the second (generally the method, but it can be the class, if the previous one was the project), and 2 the third (if the window has it, it will always be the method).

If you want all this in a simple to use helper class... here it is:

using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

internal class CodeElementDetector
{
    private readonly AsyncPackage package;
    private readonly DTE2 dte;

    public CodeElementDetector(AsyncPackage package)
    {
        this.package = package;
        dte = package.GetService<DTE, DTE2>();
    }

    public IVsCodeWindow GetActiveCodeWindow()
    {
        ThreadHelper.ThrowIfNotOnUIThread();

        // Get the IVsMonitorSelection service
        var monitorSelection = (IVsMonitorSelection)ServiceProvider.GlobalProvider.GetService(typeof(SVsShellMonitorSelection));
        if (monitorSelection == null)
            return null;

        // Get the current active IVsWindowFrame
        if (monitorSelection.GetCurrentElementValue((uint)VSConstants.VSSELELEMID.SEID_DocumentFrame, out object activeFrame) != VSConstants.S_OK)
            return null;

        if (activeFrame is not IVsWindowFrame windowFrame)
            return null;

        // Get the IVsCodeWindow from the IVsWindowFrame
        if (windowFrame.GetProperty((int)__VSFPROPID.VSFPROPID_DocView, out var docView) != VSConstants.S_OK)
            return null;

        return docView as IVsCodeWindow;
    }

    public async Task<(string className, string methodName)> GetCodeElementAtCursorAsync()
    {
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
        try
        {
            if (GetActiveCodeWindow() is not IVsDropdownBarManager manager)
                return (null, null);

            var hr = manager.GetDropdownBar(out var bar);
            if (hr != VSConstants.S_OK || bar == null)
                return (null, null);

            var part1 = GetSelectionText(bar, 0);
            var part2 = GetSelectionText(bar, 1);
            var part3 = GetSelectionText(bar, 2);

            var fqName = $"{part1}.{part2}{(part3 == null ? "" : "." + part3)}".Split('.');

            var className = fqName[fqName.Length - 2];
            var methodName = fqName[fqName.Length - 1];

            return (className, methodName);
        }
        catch (Exception ex)
        {
            Debug.WriteLine($"Error getting code element: {ex.Message}");
        }

        return (null, null);
    }

    private string GetSelectionText(IVsDropdownBar bar, int barType)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        if (bar.GetCurrentSelection(barType, out var curSelection) != VSConstants.S_OK)
            return null;
        if (bar.GetClient(out var barClient) != VSConstants.S_OK)
            return null;
        if (barClient.GetEntryText(barType, curSelection, out var text) != VSConstants.S_OK)
            return null;
        return text;
    }
}

Comments