I'm creating a Visual Studio add in, and while the basic functionality is complete, I'm trying to add some new features.
One of the features I'm planning requires the name of the method and/or class the cursor is currently located.
Take a look at the image above. It shows a Visual Studio editor with a Python file loaded. This file is not part of a project.
During my searches I encountered several references to use “FileCodeModel” or even “ITextStructureNavigatorSelectorService”.
But none of them work correctly. For example, the file shown in the image above is not part of a project, so the FileCodeModel is null for that specific file. And yet, Visual Studio shows the correct breadcrumbs at the top of the editor.
And if Visual Studio can do it, it should mean that we could query it and have this information.
What I need is to get the name of the method and/or class at the cursor in a language agnostic way. I mean, I don't want it to be specific to C# or VB. I want ti to work with Python, JavaScript, HTML, SQL, or whatever language Visual Studio supports.
How can I do it?
After some tinkering, I was able to get the information I needed.
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;
}
}