-
Notifications
You must be signed in to change notification settings - Fork 33
Modern Editor Commanding API Revisited
- Feature Team: Editor
- Owner: Oleg Tkachenko
- Document Type: Dev Design Doc
- Release: 15.6
This is an update to the original Modern Editor Commanding spec, containing revisions that are necessary to stabilize and adopt modern commanding in VS and VS for Mac.
Major changes to the original spec:
- Optional support for chained command handlers
- Contained language support (Venus/Razor)
- API changes for optimal performance
- Custom commands
- Cancellability
IOleCommandTarget is a command dispatching strategy that originated in Windows. The editor uses this interface to build up a chain of IOleCommandTargets. The Visual Studio shell translates keystrokes into commands and sends them to the first command target in our chain. The command is then passed along the chain one command target at a time. Commands often pass through command targets that have no interest in the given command at all.
Handling keystrokes can be a challenge for extenders who aren’t familiar with this interface. The simplest sample I found is located here on GitHub. Because extenders are inserting themselves into the editor’s command chain they can slow down every keystroke in Visual Studio without realizing it. There’s another example of handling commands in PTVS worth looking at.
Another downside is that it is difficult for us (the Editor Team) to accurately measure performance in a fine-grained manner. For example, currently we can see that a TypeChar was slow within Roslyn’s command target, but the only way to find out which exactly Roslyn command handler was to blame is to analyze traces, which doesn't scale for dynamic performance diagnostics purposes.
We would like to redesign commanding in the Editor with the following goals:
- Do not require IOleCommandTarget from command handlers
- It should be possible to reuse command handlers in VS for Mac and EditorTestApp
- We must be able to measure the performance of individual command handlers
- We must be able to conditionally disable command handlers
- We should be able to conditionally cancel command handlers
- Declarative: Handlers specify exactly which commands they’re interested in
- Orderable
- No chaining of command handlers is required (but possible when necessary)
- Performance must not degrade
- It’s easy to migrate existing command filters
- It’s easy to migrate existing Roslyn command handlers
We have the following non-goals from our redesigned commanding system:
- We are not trying to entirely replace IOleCommandTarget. It’s expected that customers could still use this interface if they require it. We will start deprecating it though, pushing internal partners towards modern command handlers first.
- We are not trying to noticeably improve performance of base installs of Visual Studio. Although the chaining is inefficient, most of our internal command targets are good citizens and the chain is not the source of typing delays.
- The command handlers will not run on the background thread or asynchronously.
The modern editor commanding API consists of several components:
A command is uniquely identified by a unique strong name class derived from CommandArgs.
A command binding is host specific way to bind command identifiers to host specific commands and shortcuts. For example in VS a command binding maps (guid, id) pair uniquely identifying VS command to a command identifier.
A command handler handles one or more commands uniquely identified by command identifiers.
A command handler service exposes command handlers for a given context (for example editor command handlers for a given text view).
A command handler adapter exposes modern editor commanding to a different commanding system, for example IOleCommandTarget based commanding in VS and as such allows inter command system integration (for example whole editor being a single IOleCommandTarget node in VS commanding).
CommandArgs serves as both unique command identifier and a container for command specific arguments.
CommandArgs is an abstract base class all command specific command args derive from:
namespace Microsoft.VisualStudio.Commanding
{
/// <summary>
/// A base class for all command arguments.
/// </summary>
public abstract class CommandArgs
{
}
}
EditorCommandArgs is an abstract base class all editor command args derive from. It contains common properties such as target text view and buffer.
namespace Microsoft.VisualStudio.Text.Editor.Commanding
{
/// <summary>
/// A base class for all editor command arguments.
/// </summary>
public abstract class EditorCommandArgs : CommandArgs
{
/// <summary>
/// A subject buffer to execute a command on.
/// </summary>
public ITextBuffer SubjectBuffer { get; }
/// <summary>
/// An <see cref="ITextView"/> to execute a command on.
/// </summary>
public ITextView TextView { get; }
/// <summary>
/// Creates new instance of the <see cref="EditorCommandArgs"/> with given
/// <see cref="ITextView"/> and <see cref="ITextBuffer"/>.
/// </summary>
/// <param name="textView">A <see cref="ITextView"/> to execute a command on.</param>
/// <param name="subjectBuffer">A <see cref="ITextBuffer"/> to execute command on.</param>
public EditorCommandArgs(ITextView textView, ITextBuffer subjectBuffer)
{
this.TextView = textView ?? throw new ArgumentNullException(nameof(textView));
this.SubjectBuffer = subjectBuffer ?? throw new ArgumentNullException(nameof(subjectBuffer));
}
}
}
Specific CommandArgs types can optionally contain command specific properties, e.g.:
public class TypeCharCommandArgs : EditorCommandArgs
{
public char TypedChar { get; }
public TypeCharCommandArgs(ITextView textView, ITextBuffer subjectBuffer, char typedChar) : base(textView, subjectBuffer)
{
TypedChar = typedChar;
}
}
The editor defines unique CommandArgs types for all common editor commands, but editor extensions can define custom CommandArgs types for their own commands.
ICommandHandler is an empty interface used for MEF export purposes. All command handlers indirectly implement it and exported via typeof(ICommandHandler) MEF contract.
public interface ICommandHandler
{
}
Stated goals contain a conflict between simplicity of implementing a command handler and migrating existing command filters and handlers. On one hand we want to provide a simple way to handle commands, on the other hand there is an existing rich command filter/handler ecosystem that was built on top of IOleCommandTarget and requires advanced functionality such as augmenting existing command handlers. About half of Roslyn command handlers are standalone handlers handling their own commands. But the rest half of them are handling common commands they don’t exclusively own (typically editor commands like Return or Delete) – in a sense they are customizing/augmenting those commands and implemented using command handler chaining.
To address this dichotomy, modern editor commanding supports 2 similar but distinct kinds of command handlers: a simple (ICommandHandler) and chained (IChainedCommandHandler). Simple command handlers are intended for a common case when a command handler handles its own command and has no dependencies on the rest of command handlers. Chained command handlers are intended for advanced/legacy cases of customizing/augmenting common commands such as Return.
A command handler can handle multiple commands by simply inheriting from multiple ICommandHandler or IChainedCommandHandler where each T represents each command that they handle.
A command handler is a singleton across the Visual Studio process. Any state that it stores will be visible when receiving commands from any relevant text view. If state must be associated with a given text view or text buffer, it must be stored on the text view or buffer itself. The state can then be examined by looking at the CommandArgs passed to the handler.
A command handler is exported with metadata containing:
- Unique command handler name for ordering purposes
- Content types it supports
- [Optional] Text view roles it supports
- [Optional] Ordering relatively to other command handlers
When a command is invoked or displayed in host UI (such as context menu), command handlers matching file content type, text view role and command identifier are instantiated and ordered into an implicit chain based on their specified order.
CommandState is a struct representing current command state (e.g. whether a command is enabled) as returned by a command handler at a given time:
namespace Microsoft.VisualStudio.Commanding
{
public struct CommandState
{
/// <summary>
/// If true, the command state is unspecified and should not be taken into account.
/// </summary>
public bool IsUnspecified { get; }
/// <summary>
/// If true, the command should be visible and enabled in the UI.
/// </summary>
public bool IsAvailable { get; }
/// <summary>
/// If true, the command should appear as checked (i.e. toggled) in the UI.
/// </summary>
public bool IsChecked { get; }
/// <summary>
/// If specified, returns the custom text that should be displayed in the UI.
/// </summary>
public string DisplayText { get; }
public CommandState(bool isAvailable = false, bool isChecked = false, string displayText = null, bool isUnspecified = false)
{
if (isUnspecified && (isAvailable || isChecked || displayText != null))
{
throw new ArgumentException("Unspecified command state cannot be combined with other states or command text.");
}
this.IsAvailable = isAvailable;
this.IsChecked = isChecked;
this.IsUnspecified = isUnspecified;
this.DisplayText = displayText;
}
/// <summary>
/// A helper singleton representing an available command state.
/// </summary>
public static CommandState Available { get; } = new CommandState(isAvailable: true);
/// <summary>
/// A helper singleton representing an unavailable command state.
/// </summary>
public static CommandState Unavailable { get; } = new CommandState(isAvailable: false);
/// <summary>
/// A helper singleton representing an unspecified command state.
/// </summary>
public static CommandState Unspecified { get; } = new CommandState(isUnspecified: true);
}
}
Simple command handler:
/// <summary>
/// Represents a handler for a command associated with specific <see cref="CommandArgs"/>.
/// This is a MEF component part and should be exported as the non-generic <see cref="ICommandHandler"/> with the following
/// attributres:
///
/// [Export(typeof(ICommandHandler))]
/// [Name(nameof(ExpandContractSelectionCommandHandler))]
/// [ContentType("text")]
/// [TextViewRole(PredefinedTextViewRoles.PrimaryDocument)]
/// [TextViewRole(PredefinedTextViewRoles.EmbeddedPeekTextView)]
/// [Order(Before ="OtherCommandHandler")]
/// [HandlesCommandArgs(typeof(ExpandSelectionCommandArgs))]
/// class ExpandSelectionCommandHandler : ICommandHandler<ExpandSelectionCommandArgs>
/// </summary>
public interface ICommandHandler<T> : ICommandHandler where T : CommandArgs
{
/// <summary>
/// Called to determine the state of the command.
/// </summary>
/// <param name="args">The <see cref="CommandArgs"/> arguments for the command.</param>
/// <returns>A <see cref="CommandState"/> instance that contains information on the availability of the command.</returns>
CommandState GetCommandState(T args);
/// <summary>
/// Called to execute the command.
/// </summary>
/// <param name="args">The <see cref="CommandArgs"/> arguments for the command.</param>
/// <returns>Returns <c>true</c> if the command was handled, <c>false</c> otherwise.</returns>
bool ExecuteCommand(T args, CommandExecutionContext commandExecutionContext);
/// <summary>
/// Gets display name of the command handler used to represent it to the user, for
/// example when blaming it for delays or used in commanding diagnostics.
/// </summary>
string DisplayName { get; };
}
Chained command handlers follow existing Roslyn command handler semantics:
/// <summary>
/// Represents a command handler that depends on behavior of following command handlers in the command execution chain
/// formed from same strongly-typed <see cref="ICommandHandler"/>s ordered according to their [Order] attributes.
/// </summary>
/// <remarks>
/// This is a MEF component part and should be exported as the non-generic <see cref="ICommandHandler"/> with required
/// [Name], [ContentType] attributes and optional [Order] and [TextViewRole] attributes.
/// </remarks>
/// <example>
/// [Export(typeof(ICommandHandler))]
/// [Name(nameof(MyCommandHandler))]
/// [ContentType("text")]
/// [Order(Before ="OtherCommandHandler")]
/// [TextViewRole(PredefinedTextViewRoles.Editable)]
/// internal class MyCommandHandler : IChainedCommandHandler<MyCommandArgs>
/// </example>
public interface IChainedCommandHandler<T> : ICommandHandler, INamed where T : CommandArgs
{
/// <summary>
/// Called to determine the state of the command.
/// </summary>
/// <param name="args">The <see cref="CommandArgs"/> arguments for the command.</param>
/// <param name="nextCommandHandler">The next command handler in the command execution chain.</param>
/// <returns>A <see cref="CommandState"/> instance that contains information on the availability of the command.</returns>
CommandState GetCommandState(T args, Func<CommandState> nextCommandHandler);
/// <summary>
/// Called to execute the command.
/// </summary>
/// <param name="args">The <see cref="CommandArgs"/> arguments for the command.</param>
/// <param name="nextCommandHandler">The next command handler in the command execution chain.</param>
void ExecuteCommand(T args, Action nextCommandHandler, CommandExecutionContext executionContext);
}
With the above types, we can now create command handlers that meet all of the stated design goals. For example, here is a sample command handler that targets the C# content type, and handles Expand/Contract Selection commands:
public class ExpandSelectionCommandArgs: EditorCommandArgs
{
public ExpandSelectionCommandArgs(ITextView textView, ITextBuffer subjectBuffer) : base(textView, subjectBuffer)
{
}
}
[Export(typeof(ICommandHandler))]
[Name(nameof(ExpandContractSelectionCommandHandler))]
[ContentType("any" )]
[TextViewRole(PredefinedTextViewRoles.PrimaryDocument)]
[TextViewRole(PredefinedTextViewRoles.EmbeddedPeekTextView)]
internal sealed class ExpandContractSelectionCommandHandler
: ICommandHandler<ExpandSelectionCommandArgs>, ICommandHandler<ContractSelectionCommandArgs>
{
[ImportingConstructor]
public ExpandContractSelectionCommandHandler(…)
{
…
}
public CommandState GetCommandState(ExpandSelectionCommandArgs args)
{
var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState(
args.TextView,
this.EditorOptionsFactoryService,
this.NavigatorSelectorService);
return storedCommandState.GetExpandCommandState(args.TextView);
}
public CommandState GetCommandState(ContractSelectionCommandArgs args)
{
var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState(
args.TextView,
this.EditorOptionsFactoryService,
this.NavigatorSelectorService);
return storedCommandState.GetContractCommandState(args.TextView);
}
public bool ExecuteCommand(ExpandSelectionCommandArgs args, CommandExecutionContext commandExecutionContext)
{
var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState(
args.TextView,
this.EditorOptionsFactoryService,
this.NavigatorSelectorService);
return storedCommandState.ExpandSelection(args.TextView);
}
public bool ExecuteCommand(ContractSelectionCommandArgs args, CommandExecutionContext commandExecutionContext)
{
var storedCommandState = ExpandContractSelectionImplementation.GetOrCreateExpandContractState(
args.TextView,
this.EditorOptionsFactoryService,
this.NavigatorSelectorService);
return storedCommandState.ContractSelection(args.TextView);
}
public string DisplayName => Strings.ExpandContractSelectionCommandHandlerName;
}
Scenario: Customer wants to create simple GoToDefinitionCommandHandler. For source see: http://source.roslyn.io/#Microsoft.CodeAnalysis.EditorFeatures/GoToDefinition/GoToDefinitionCommandHandler.cs,18
- User presses F12 in C# document
- Shell translates F12 to pair (VSConstants.VSStd97CmdID, VSConstants.VSStd97CmdID.GotoDefn)
- Shell invokes IOleCommandTarget.Exec on primary editor command target
- Editor command target translates (Guid, Id) pair to GoToDefinitionCommandArgs
- Editor gets all command handlers who a. Have declared interest in GoToDefinitionCommandArgs b. Have specified a CSharp content type or base type (text, any etc.) c. Have specified a relevant text view role
- Editor orders them according to any [Order] attributes and forms an implicit chain
- Editor invokes first handler in the chain.
Per Roslyn adoption of the modern commanding (Web view), about half of all Roslyn command handlers depend on chaining with the following command handlers. One distinctive characteristic of those is that they are targeting common commands they don’t exclusively own (typically editor commands like Return or Delete) – in a sense they are customizing/augmenting those commands. There are 3 kind of dependency:
- Some handlers rely on the following chain to enable a command as they don’t have enough information to make that decision on their own.
- Some handlers need to perform a post action after following chain have finished handling a command
- And in some special cases handlers need to execute following handlers more than once. For example, Roslyn completion typechar command handler needs to let the following handlers to handle the typechar command (including editor built-in typechar command filters like brace completion and buffer operations), then get control back to undo it all, commit a completion and finally let the following handlers handle the typechar again.
Those requirements cannot be satisfied by just ordering handlers or calling handlers by name.
It’s reasonable to assume that a typical language service would have a similar proportion of chained handlers so we cannot realistically claim these are just special cases. These are typical cases for any non-trivial language service so we need to provide first class support for such handlers. Also instead of coming up with 3 different ways to address aforementioned 3 kinds of dependencies, we should just enable then all in a simple manner, by providing IChainedCommandHandler support.
Editor Command Handler service exposes command handlers for a given text view and so allows hosts plugin command handlers into host specific command execution. For example in VS editor uses it to plugin command handlers into editor’s command filter.
/// <summary>
/// A factory producing <see cref="IEditorCommandHandlerService"/> used to execute commands in a given text view.
/// </summary>
/// <remarks>
/// This is a MEF component and should be imported as
///
/// [Import]
/// private IEditorCommandHandlerServiceFactory factory;
/// </remarks>
public interface IEditorCommandHandlerServiceFactory
{
/// <summary>
/// Gets or creates a <see cref="IEditorCommandHandlerService"/> for a given <see cref="ITextView"/>.
/// </summary>
/// <param name="textView">A text view to get or create <see cref="IEditorCommandHandlerService"/> for.</param>
IEditorCommandHandlerService GetService(ITextView textView);
/// <summary>
/// Gets or creates a <see cref="IEditorCommandHandlerService"/> for a given <see cref="ITextView"/> and <see cref="ITextBuffer"/>.
/// </summary>
/// <param name="textView">A text view to get or create <see cref="IEditorCommandHandlerService"/> for.</param>
/// <param name="subjectBuffer">A text buffer to get or create <see cref="IEditorCommandHandlerService"/> for.</param>
IEditorCommandHandlerService GetService(ITextView textView, ITextBuffer subjectBuffer);
}
/// <summary>
/// A service to execute commands on a text view.
/// </summary>
/// <remarks>
/// Instance of this service are created by <see cref="IEditorCommandHandlerServiceFactory"/>.
/// </remarks>
public interface IEditorCommandHandlerService
{
/// <summary>
/// Get the <see cref="CommandState"/> for command handlers of a given command.
/// </summary>
/// <param name="argsFactory">A factory of <see cref="EditorCommandArgs"/> that specifies what kind of command is being queried.</param>
/// <param name="nextCommandHandler">A next command handler to be called if no command handlers were
/// able to determine a command state.</param>
/// <typeparam name="T">Tehe </typeparam>
/// <returns>The command state of a given command.</returns>
CommandState GetCommandState<T>(Func<ITextView, ITextBuffer, T> argsFactory, Func<CommandState> nextCommandHandler) where T : EditorCommandArgs;
/// <summary>
/// Execute a given command on the <see cref="ITextView"/> associated with this <see cref="IEditorCommandHandlerService"/> instance.
/// </summary>
/// <param name="argsFactory">A factory of <see cref="EditorCommandArgs"/> that specifies what kind of command is being executed.
/// <paramref name="nextCommandHandler">>A next command handler to be called if no command handlers were
/// able to handle a command.</paramref>
void Execute<T>(Func<ITextView, ITextBuffer, T> argsFactory, Action nextCommandHandler) where T : EditorCommandArgs;
}
Current commanding model in Venus/Razor scenario is based on IOleCommandTarget based filtering and IVsContainedLanguage.
- All commands for an aspx file are handled by a command filter added to the text views’s command chain (call it Venus command filter)
- For specific commands, when the caret is positioned in code blocks, Venus command filter routes them to the contained language command filter. This is done via legacy IVsContainedLanguage feature.
- Roslyn implements IVsContainedLanguage and exposes its own IOleCommandTarget that internally dispatches to Roslyn command handlers.
Ideally we would modify Web Tools to use ICommandHandlerService to route commands to Roslyn, but in reality this is hardly feasible due to aspx editor being native and in deep maintenance mode.
The short-term solution is to only modify Roslyn side, without touching aspx or chtml editors. To facilitate that we provide a command handler service adapter component that allows to expose modern command handlers via IOleCommandTarget interface. That adapter is also used in the editor itself (after all the whole editor is just an IOleCommandTarget from VS shell point of view).
/// <summary>
/// An adapter that exposes <see cref="IEditorCommandHandlerService"/>s for a given text view via <see cref="IOleCommandTarget"/> interface.
/// </summary>
public interface IVsCommandHandlerServiceAdapter : IOleCommandTarget
{
/// <summary>
/// The text view this adapter was created for.
/// </summary>
ITextView TextView { get; }
/// <summary>
/// A next <see cref="IOleCommandTarget"/> in a command handling chain.
/// </summary>
IOleCommandTarget NextCommandTarget { get; }
}
/// <summary>
/// A factory service for creating <see cref="IVsCommandHandlerServiceAdapter"/>s. Such adapters are used to
/// expose <see cref="IEditorCommandHandlerService"/>s for a given text view in <see cref="IOleCommandTarget"/> based
/// commanding intefaces, such as <see cref="IVsContainedLanguage"/>.
/// </summary>
/// <remarks>This is a MEF Component, and should be exported with the following attribute:
/// [Export(typeof(IVsCommandHandlerServiceAdapterFactory))]
/// </remarks>
public interface IVsCommandHandlerServiceAdapterFactory
{
/// <summary>
/// Creates an <see cref="IVsCommandHandlerServiceAdapter"/> instance wrapping <see cref="IEditorCommandHandlerService"/> for
/// a given <see cref="ITextView"/>.
/// </summary>
/// <param name="textView">A text view to create <see cref="IVsCommandHandlerServiceAdapter"/> for.</param>
/// <param name="nextCmdTarget">A next command target to delegate unhandled commands.</param>
IVsCommandHandlerServiceAdapter Create(ITextView textView, IOleCommandTarget nextCmdTarget);
/// <summary>
/// Creates an <see cref="IVsCommandHandlerServiceAdapter"/> instance wrapping <see cref="IEditorCommandHandlerService"/> for
/// a given <see cref="ITextView"/> and <see cref="IVsTextBuffer"/>.
/// </summary>
/// <param name="textView">A text view to create <see cref="IVsCommandHandlerServiceAdapter"/> for.</param>
/// <param name="subjectBuffer">A subject text buffer to create <see cref="IVsCommandHandlerServiceAdapter"/> for.</param>
/// <param name="nextCmdTarget">A next command target to delegate unhandled commands.</param>
IVsCommandHandlerServiceAdapter Create(ITextView textView, ITextBuffer subjectBuffer, IOleCommandTarget nextCmdTarget);
}
Adding a new custom command can be broken down to the following tasks:
- Command definition (IDE specific). For example, in VS this consists of authoring a new command in a vsct file.
- Command identification (Cross IDE). Define a new type derived from CommandArgs or from a known type like EditorCommandArgs that would serve as both command id and a container for a command input arguments.
- Provide optional CommandArgs factory (IDE specific)
If command-identifying CommandArgs type has default constructor or derives from a known type such as EditorCommandArgs, it will be instantiated via reflection.
Otherwise, a CommandArgs factory needs to be exported. Such a factory lets create CommandArgs instances using custom logic that utilizes IDE specific command input such as typed char in TypeCharCommandArgs or command arguments when a command is called via Command Window in VS. In VS such a factory gets access to IOleCommandTarget.Exec’s nCmdexecopt and pvaIn arguments.
public interface IVsCommandArgsFactory
{
}
public interface IVsEditorCommandArgsFactory<T> : IVsCommandArgsFactory where T : EditorCommandArgs
{
T CreateCommandArgs(ITextView textView, ITextBuffer subjectBuffer, uint nCmdexecopt, IntPtr pvaIn);
}
Here is a sample of a custom CommandArgs that initializes it using command input:
[Export(typeof(IVsCommandArgsFactory))]
[Name("Default VS command arguments factory")]
[CommandArgsType(typeof(TypeCharCommandArgs))]
internal class VsEditorCommandArgsFactory :
IVsEditorCommandArgsFactory<TypeCharCommandArgs>
{
public TypeCharCommandArgs CreateCommandArgs(ITextView textView, ITextBuffer subjectBuffer, uint nCmdexecopt, IntPtr pvaIn)
{
var typedChar = (char)(ushort)Marshal.GetObjectForNativeVariant(pvaIn);
return new TypeCharCommandArgs(textView, subjectBuffer, typedChar);
}
}
- Command handling (Cross IDE). Define and export command handler for given command identifier.
- Command binding (IDE specific)
Binding of IDE specific command id (guid/id pair in VS for example) to a command identifier is IDE specific functionality. In VS this is done by exporting [CommandBinding(guid, id, Type)] attribute that maps VS command’s guid/id pair to a CommandArgs type, other hosts such as Visual Studio for Mac would need to define its own command binding logic.
internal sealed class CommandBindings
{
[Export]
[CommandBinding(EditorConstants.EditorCommandSetString, (uint)EditorConstants.EditorCommandID.DuplicateSelection, typeof(DuplicateSelectionCommandArgs))]
internal CommandBindingDefinition editorCommandBindings;
}
Even though command handlers are executed synchronously on the UI thread, we would like to bake in an ability for command handler service host to cancel execution of a command handler if needed. A typical scenario is a command handler participating in a typing commands (for example doing some auto formatting on return key) and introducing typing delays due to waiting for some background parsing to complete before it can properly format. If user chooses to cancel it, command handler service would attempt to cancel this command handler execution.
Of course, given that command handlers are being executed synchronously on the UI thread, supporting cancellation would require command handlers to collaborate, there is no way to cancel command handler execution externally.
To facilitate rich interactive two way shared cancellability, each command handler is passed IUIThreadOperationContext instance to the Execute method and is expected to honor a request to cancel execution when running potentially long operations. This IUIThreadOperationContext instance allows a command handler to influence shared wait context by indicating whether an execution can be cancelled, by providing specific message describing long operation and providing progress tracking info.
/// <summary>
/// Represents a context of executing potentially long running operation on the UI thread, which
/// enables shared two way cancellability and wait indication.
/// </summary>
/// <remarks>
/// Instances implementing this interface are produced by <see cref="IUIThreadOperationExecutor"/>
/// MEF component.
/// </remarks>
public interface IUIThreadOperationContext : IPropertyOwner, IDisposable
{
/// <summary>
/// Cancellation token that allows user to cancel the operation unless the operation
/// is not cancellable.
/// </summary>
CancellationToken UserCancellationToken { get; }
/// <summary>
/// Gets whether the operation can be cancelled.
/// </summary>
/// <remarks>This value is composed of initial AllowCancellation value and
/// <see cref="IUIThreadOperationScope.AllowCancellation"/> values of all currently added scopes.
/// The value composition logic takes into acount disposed scopes too - if any of added scopes
/// were disposed while its <see cref="IUIThreadOperationScope.AllowCancellation"/> was false,
/// this property will stay false regardless of all other scopes' <see cref="IUIThreadOperationScope.AllowCancellation"/>
/// values.
/// </remarks>
bool AllowCancellation { get; }
/// <summary>
/// Gets user readable operation description, composed of initial context description and
/// descriptions of all currently added scopes.
/// </summary>
string Description { get; }
/// <summary>
/// Gets current list of <see cref="IUIThreadOperationScope"/>s in this context.
/// </summary>
IEnumerable<IUIThreadOperationScope> Scopes { get; }
/// <summary>
/// Adds a UI thread operation scope with its own two way cancellability, description and progress tracker.
/// The scope is removed from the context on dispose.
/// </summary>
IUIThreadOperationScope AddScope(bool allowCancellation, string description);
/// <summary>
/// Allows a component to take full ownership over this UI thread operation, for example
/// when it shows its own modal UI dialog and handles cancellability through that dialog instead.
/// </summary>
void TakeOwnership();
}
/// <summary>
/// Represents a single scope of a context of executing potentially long running operation on the UI thread.
/// Scopes allow multiple components running within an operation to share the same context.
/// </summary>
public interface IUIThreadOperationScope : IDisposable
{
/// <summary>
/// Gets or sets whether the operation can be cancelled.
/// </summary>
bool AllowCancellation { get; set; }
/// <summary>
/// Gets user readable operation description.
/// </summary>
string Description { get; set; }
/// <summary>
/// The <see cref="IUIThreadOperationContext" /> owning this scope instance.
/// </summary>
IUIThreadOperationContext Context { get; }
/// <summary>
/// Progress tracker instance to report operation progress.
/// </summary>
IProgress<ProgressInfo> Progress { get; }
}
/// <summary>
/// Represents an update of a progress.
/// </summary>
public struct ProgressInfo
{
/// <summary>
/// A number of already completed items.
/// </summary>
public int CompletedItems { get; }
/// <summary>
/// A total number if items.
/// </summary>
public int TotalItems { get; }
/// <summary>
/// Creates a new instance of the <see cref="ProgressInfo"/> struct.
/// </summary>
/// <param name="completedItems">A number of already completed items.</param>
/// <param name="totalItems">A total number if items.</param>
public ProgressInfo(int completedItems, int totalItems)
{
this.CompletedItems = completedItems;
this.TotalItems = totalItems;
}
}
IUIThreadOperationContext implementation is IDE specific, for example in VS we are using threaded wait dialog based implementation.
Typical cancellation rules apply to command handlers:
- Throw OperationCanceledException (waitContext.CancellationToken.ThrowIfCancellationRequested()) when command handling operation was successfully cancelled
- Know when you’ve passed the point of no cancellation. Don’t cancel if you’ve already incurred side-effects that command handler isn’t prepared to revert on the way out that would leave system in an inconsistent state. So if you’ve done some work, and have a lot more to do, and the token is cancelled, you must only cancel when and if you can do so leaving objects in a valid state. This may mean that you have to finish the large amount of work, or undo all your previous work (i.e. revert the side-effects), or find a convenient place that you can stop halfway through but in a valid condition, before then throwing OperationCanceledException. In other words, the command handler service host must be able to recover to a known consistent state after cancelling your work, or realize that cancellation was not responded to and that the caller then must decide whether to accept the work, or revert its successful completion on its own.
- Propagate CancellationToken or IWaitContext to all the methods you call that accept one, except after the “point of no cancellation” referred to in the previous point.
- Don’t throw OperationCanceledException after you’ve completed the work, just because the token was signaled. Return a successful result and let the command handler service decide what to do next. The caller can’t assume you’re cancellable at a given point anyway so they have to be prepared for a successful result even upon cancellation.
- When chained command handler calls next handler and already did some work that require clean up on cancellation, it should either disable cancellation or catch OperationCanceledException thrown by next handlers to perform its cleanup. If a cancellation was requested while next handler was called, but no OperationCanceledException was thrown by following handlers, a chained handler needs to decide whether to cancel its own execution based on rule 2 above.
In some scenarios we’d want to recover from command cancellation, for example consider a scenario of a command handler doing auto-formatting on Return command. If the command handler is waiting for parsing to complete before passing control to the editor command handler and user cancels it, we still want editor to handle Return.
In VS 2017 15.6 we will have a simple implementation of the recovery: if command is cancelled successfully, it’s recovered in host specific way. In VS it will be recovered by letting command still be handled by built-in command filters in editor IOleCommandTarget chain (basically skipping command handler service).
The editor maintains its own internal chain of command filters, which dictates the order in which filters and Roslyn command handlers handle commands. Introduction of modern command handlers, migrating command filters and Roslyn command handlers might change this order. The goal is to preserve the order as much as possible.
Typical existing order of execution (top to bottom). Note that this order is following text view typical text view initialization, but extensions can add themselves at #1 later (IVsTextView.AddCommandFilter always adds to the position #1): 0. Prefix command filter (e.g. Peek)
- Language Service command filters (typically added on language service initialization in IVSCodeWindowManager.AddAdornments() callback). 1.1 Roslyn command handlers 1.2 Or Contained language command filter (Venus) 1.2.1 Roslyn command handlers on the secondary buffer
- Command filters added via ITextViewCreationListener
- Built-in editor command filters: bookmarks, Task List, Brace Completion, Intellisense, Undo
- View Adapter itself (editor operations)
The new order of execution, with modern command handlers added to the chain: 0. Prefix command filter (e.g. Peek)
- Language Service command filters (typically added on language service initialization in IVSCodeWindowManager.AddAdornments() callback). 1.1 Roslyn command handlers 1.2 Or Contained language command filter (Venus) 1.2.1 Roslyn command handlers on the secondary buffer
- Modern command handlers
- Command filters added via ITextViewCreationListener
- Built-in editor command filters: bookmarks, Task List, Brace Completion, Intellisense, Undo
- View Adapter itself (editor operations)
Then migrating 3rd party command filter to modern command handler will move it up from #3 to #2. Migrating Roslyn command handler will move it down from #1.1 to #2.
Q: so in the projection buffer case, how subjectBuffer is selected? Will the editor one always create the chain for the surface buffer, and it’s up to that to forward to subject buffers? Or will this chase directly?
Here is an overview of the commanding in projection scenarios before modern commanding is introduced:
- Editor command filters by default are not buffer affinitized and exist on text view level. They are added into a text view’s filter chain and called on every command regardless of text view’s buffer graph, caret or selection. It’s a responsibility of a command filter to handle projection scenarios. That’s how Venus/Razor command handling is implemented – a dedicated command filter that knows how to deal with projection and routes commands to contained languages based on caret and selection buffer mapping.
- There is special kind of editor command filters, added via IVsTextView3.AddProjectionAwareCommandFilter(). Those filters are called only when text view's caret and selection map cleanly to the view's DataBuffer. Diff’s command filter is the only such filter in VS. (Is it used anywhere else but inline diff view?)
- Roslyn command handlers are content type affinitized and called only when text view’s caret can be mapped (down or up) to any buffer of the Roslyn content type.
- In Venus scenario, Venus command filter (see #1) routes commands down to Roslyn command handlers based on its own logic (mapping caret/selection) via IVsContainedLanguage.GetTextViewFilter(). In this case Roslyn command handlers operate on the secondary buffer (as IVsTextBufferCoordinator.GetSecondaryBuffer(text view)).
Requirements for modern editor command handlers support in projection scenarios:
- Command handlers are content type affinitized
- Command handlers targeting subject buffer content types just work in projection scenario
- There is a meaningful order of executing command handlers targeting different content types in projection scenario
- Venus/Razor scenario is not broken
- Venus/Razor in VS and VS for Mac benefit from editor command handler projection support
Resolution:
- Introduce a concept of “buffer resolver” extensibility point, which, given a text view would be responsible to determine which buffers should a command be routed to. Default implementation will determine affected buffers based on caret position mapping (basically following Razor behavior).
- At command execution time construct a dynamic chain of command handlers based on the following inputs: buffer graph trimmed to buffers affected (based on buffer resolvers), content types graph, command handler Order metadata:
- For each buffer in the trimmed buffer graph in topological order
- For each content type in the hierarchy of content types of this buffer’s content types ordered from most specific to least specific
- Select command handlers targeting this content type, order by Order metadata
- For each content type in the hierarchy of content types of this buffer’s content types ordered from most specific to least specific
For example, Razor view with caret being inside C# block: Buffer graph:
Trimmed buffer graph (based on buffer resolvers):
Razor Projection
/ |
C# |
/ \ |
Inert HTML
Ordered graphs: Razor projection, C#, HTML
Ordered buffers and command handlers:
Razor Projection C# HTML
1. RazorCS 3. CSharp 5. HTML
2. Projection 4. Code
7. Any 6. Text
- Incentivize partners and 3rd party extenders to migrate by enabling command handler reuse between VS and VS for Mac.
- Update MSDN documentation and samples to use modern command handlers
- Add “Editor Custom Command” VSIX item template to VS SDK and so incentivize extenders to use it.
- Update IVSTextView.AddCommandFilter() MSDN documentation to indicate it’s obsolete and there is a simple way to handle editor commands.
- Do not deprecate IVSTextView.AddCommandFilter(). First – this is IDL defined and there is not simple way to mark it as obsolete. Second - C++ will still like to keep using it.
- Push on internal partners to migrate using typing perf and impossibility of fine-grained typing perf diagnostics as reasons.
- [P2] Implement automatic shimming of modern command handlers for previous VS versions, ideally by just referencing a Nuget package. Tracked by Task 529883.
- Add an ability for command handlers to be UIContext bound in VS. Then we can save on loading them aggressively