Search code examples
c#visual-studiovsix

How do I handle multiple carets?


I'm developing a Visual Studio Extension/VSIX package for work that involves changing the caret's position. Of course, dealing with a single caret is easy:

/// Get the Host of current active view
private async Task<IWpfTextViewHost> GetCurrentViewHostAsync()
{
    // First get the active view:
    var txtManager = (IVsTextManager)await ServiceProvider.GetServiceAsync(typeof(SVsTextManager));
    Assumes.Present(txtManager);
    IVsTextView vTextView = null;
    const int mustHaveFocus = 1;
    textManager.GetActiveView(mustHaveFocus, null, out vTextView);

    if (vTextView is IVsUserData userData)
    {
        // Get host
        IWpfTextViewHost viewHost;
        object holder;
        Guid guidViewHost = DefGuidList.guidWpfTextViewHost;
        userData.GetData(ref guidViewHost, out holder);
        viewHost = (IWpfTextViewHost)holder;

        return viewHost;
    }

    return null;
}

// Later:
private async void ExecCommand(object sender, EventArgs e)
{
    IWpfTextViewHost host = await GetCurrentViewHostAsync();

    // Access single caret
    host.TextView.Caret.MoveTo(/* New position here */);
}

But let's say I use the "Insert Next Matching Caret" command to insert another caret, so I have two different carets now. Moving the caret using the above method will remove the second caret, and as far as I can tell, IVsTextView only has a single Caret property.

My next idea was that I'm supposed to access other cursors with other IVsTextManager interfaces in the host, but the closest thing I can find is its EnumViews(IVsTextBuffer, IVsEnumTextViews) method, which always returns some negative, non-S_OK value and leaves the IVsEnumTextViews item as null. A similar thing happens with the EnumIndependentViews method.

Am I approaching this right? How does the "Multiple Carets" thing even work? I can't find any documentation on it. Does the API even let me do this?


Solution

  • It turns out you have to use ITextView2's (Not ITextView) MultiSelectionBroker property, which can only be accessed with TextView's GetMultiSelectionBroker Extension method. See docs for more details on it.

    private async void ExecCommand(object sender, EventArgs e)
    {
        // ConfigureAwait(true) is just to silence warnings about adding ConfigureAwait(false)
        // "false" will cause this to not function properly. (Carets won't update).
        IWpfTextViewHost host = await GetCurrentViewHostAsync().ConfigureAwait(true);
    
        // Because TextView is only ITextView and not ITextView2, 
        //  we can only access the MultiSelectionBroker property with the extension method:
        IMultiSelectionBroker selectionBroker = host.TextView.GetMultiSelectionBroker();
        foreach (var selection in selectionBroker.AllSelections)
        {
            var nextPoint = GetSomePointHere(host, selection);
            selectionBroker.TryPerformActionOnSelection(
                selection,
                t => t.MoveTo(nextPoint, false, PositionAffinity.Successor),
                out Selection after
            );
        }
    }
    

    Don't know why, but I must have completely missed the docs for ITextView2 when I wrote the question.

    Note: The default Microsoft.VisualStudio.Text.UI.dll will not contain this method because it is an extension method. You can remove the dll reference from the solution and re-add the correct one (under Extensions) to fix this.