forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commands
Joyce Er edited this page Jul 1, 2019
·
8 revisions
The VCS Commands infrastructure is used within the Python Extension to implement the publish-subscribe messaging pattern. (The primary benefit is decoupled architecture).
- CommandRegistry in (src/client/common/commandRegistry.ts) A convenient location for registering of commands with their respective handlers. Note: This class implements the IExtensionActivationService interface. See Activation for further information.
- Register commands with their handlers in the above CommandRegistry class.
- Use
pub-subpattern for loose coupling. - When adding a new command or invoking a new VS Code command, ensure the corresponding entry exists in the following type:
// Add items in here only for commands that do not have any arguments
interface ICommandNameWithoutArgumentTypeMapping {
[Commands.Set_Interpreter]: [];
[Commands.Run_Linter]: [];
}
// Add items in here for commands that have arguments, and add the type definitions for the arguments as well.
export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping {
['setContext']: [string, boolean];
['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }];
[Commands.Sort_Imports]: [undefined, Uri];
[Commands.Exec_In_Terminal]: [undefined, Uri];
[Commands.Tests_ViewOutput]: [undefined, CommandSource];
[Commands.Tests_Stop]: [undefined, Uri];
}- When adding handlers for custom commands, try to leave an empty first argument of
undefined.- Why:
- Assume a command is hooked to a UI element such as Tree Node (File Explorer, Test Explorer, etc)
- When this command is invoked, the first argument passed into the command is the data associated with the
Tree Node. - In the case of the
File ExplorertheUriof the selected file is passed. - In the case of the
Test Explorerthe underlyingDataassociated with the node is passed. - Thus adding a blank argument for a node makes the arguments future proof, or you can always bear the above in mind.
The VSC API sits behind an interface named ICommandManager.
Unfortunately the methods used to execute a command (executeCommand) and adding of handlers (callbacks) for commands are not strongly typed (see below). This has lead to a few bugs in the past.
export interface ICommandManager {
registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable;
executeCommand<T>(command: string, ...rest: any[]): Thenable<T | undefined>;
}As can be seen above the arguments passed are loosely typed (any), same with the arguments passed into the handler.
commandManager.registerCommand('HelloWorld', (name: string, age: number) => {
console.log(`Hello ${name}, your age is ${age}`);
});
commandManager.executeCommand('HelloWorld', false, 'Bye');- Considering the above sample, TypeScript will not provide any warnings about passing incorrect arguments to the command
Hello Word. - All is good during compile time
- However during runtime, the code could fall over, due to unexpected types.
- Add strong typing to the methods
executeCommandandregisterCommand. - Ensure the correct arguments are passed for a specific command
- Ensure the signature of the command handler is correct
This is achieved using type inference in TypeScript, more information can be found here Advanced Types.
- We will keep track of all commands that will be invoked in code without passing any arguments.
- This will be defined as a union of string literal types. E.g.
type Command = 'HelloWorld' | 'command_name_1' | 'command_name_2' | 'command_name_3';
const myCommand: Command = 'command_name_1';
const myCommand: Command = 'something'; // Typescript compiler will throw errors.- Following the previous sample, we can define a type that defines the arguments for the command
HelloWorld - As follows:
type HelloWorldArguments = [string, number];- Next step, we keep track of the command and the argument types.
- The mapped list is maintained in a simple
typedictionary (an interface). - Note: It cannot be done in a literal dictionary, as typing (type hints) are compile time, not run time.
- This is achieved today as follows:
export type CommandsWithoutArgs = keyof ICommandNameWithoutArgumentTypeMapping;
interface ICommandNameWithoutArgumentTypeMapping {
[Commands.Set_Interpreter]: [];
[Commands.Run_Linter]: [];
}
export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping {
['setContext']: [string, boolean];
['revealLine']: [{ lineNumber: number; at: 'top' | 'center' | 'bottom' }];
[Commands.Sort_Imports]: [undefined, Uri];
[Commands.Exec_In_Terminal]: [undefined, Uri];
[Commands.Tests_ViewOutput]: [undefined, CommandSource];
[Commands.Tests_Stop]: [undefined, Uri];
}- The interface
ICommandNameWithoutArgumentTypeMappingkeeps track of all commands that do not have any arguments. - The interface
ICommandNameArgumentTypeMappingkeeps track of all commands along with their argument definitions.- As we have commands that have arguments, this is a super set of
ICommandNameWithoutArgumentTypeMapping(hence the inheritance). - Not having arguments is the same as having an arguments length of
0, hence the empty array[]
- As we have commands that have arguments, this is a super set of
- These interfaces have a name value pair of the
commandand the type definition for thearguments. - The type
CommandsWithoutArgsis a list of all commands that do not have any arguments (used in other parts of the code, this is basically the same as hardcoding aunion literal string, we're just inferring usingkeyof). - Finally all of the above is put together as follows:
export interface ICommandManager {
registerCommand<E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>(command: E, callback: (...args: U) => any, thisArg?: any): Disposable;
executeCommand<T, E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>(command: E, ...rest: U): Thenable<T | undefined>;
}
// Now using the previous sample:
commandManager.registerCommand('HelloWorld', (name: string, age: number) => {
console.log(`Hello ${name}, your age is ${age}`);
});
commandManager.executeCommand('HelloWorld', false, 'Bye'); // Compiler will throw errors.
- With strong typing (type checking) we get the benefits of compile time type checks
- Intellisense for the command handlers
- Intellisense for the command arguments
See below, for samples on intellisense for command handlers and when passing arguments:
