forked from DonJayamanne/pythonVSCode
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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-sub
pattern 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 Explorer
theUri
of the selected file is passed. - In the case of the
Test Explorer
the underlyingData
associated 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
executeCommand
andregisterCommand
. - 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
type
dictionary (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
ICommandNameWithoutArgumentTypeMapping
keeps track of all commands that do not have any arguments. - The interface
ICommandNameArgumentTypeMapping
keeps 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
command
and the type definition for thearguments
. - The type
CommandsWithoutArgs
is 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: