You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

215 lines
5.2 KiB

5 years ago
5 years ago
  1. import { commands, Disposable } from 'vscode';
  2. import { ContextKeys, setContext } from './constants';
  3. import { Logger } from './logger';
  4. import { log } from './system/decorators/log';
  5. export declare interface KeyCommand {
  6. onDidPressKey?(key: Keys): void | Promise<void>;
  7. }
  8. const keyNoopCommand = Object.create(null) as KeyCommand;
  9. export { keyNoopCommand as KeyNoopCommand };
  10. export const keys = [
  11. 'left',
  12. 'alt+left',
  13. 'ctrl+left',
  14. 'right',
  15. 'alt+right',
  16. 'ctrl+right',
  17. 'alt+,',
  18. 'alt+.',
  19. 'escape',
  20. ] as const;
  21. export type Keys = typeof keys[number];
  22. export type KeyMapping = { [K in Keys]?: KeyCommand | (() => Promise<KeyCommand>) };
  23. type IndexableKeyMapping = KeyMapping & {
  24. [index: string]: KeyCommand | (() => Promise<KeyCommand>) | undefined;
  25. };
  26. const mappings: KeyMapping[] = [];
  27. export class KeyboardScope implements Disposable {
  28. private readonly _mapping: IndexableKeyMapping;
  29. constructor(mapping: KeyMapping) {
  30. this._mapping = mapping;
  31. for (const key in this._mapping) {
  32. this._mapping[key] = this._mapping[key] ?? keyNoopCommand;
  33. }
  34. mappings.push(this._mapping);
  35. }
  36. @log({
  37. args: false,
  38. prefix: context => `${context.prefix}[${mappings.length}]`,
  39. })
  40. async dispose() {
  41. const index = mappings.indexOf(this._mapping);
  42. const cc = Logger.getCorrelationContext();
  43. if (cc != null) {
  44. cc.exitDetails = ` \u2022 index=${index}`;
  45. }
  46. if (index === mappings.length - 1) {
  47. mappings.pop();
  48. await this.updateKeyCommandsContext(mappings[mappings.length - 1]);
  49. } else {
  50. mappings.splice(index, 1);
  51. }
  52. }
  53. private _paused = true;
  54. get paused() {
  55. return this._paused;
  56. }
  57. @log<KeyboardScope['clearKeyCommand']>({
  58. args: false,
  59. prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`,
  60. })
  61. async clearKeyCommand(key: Keys) {
  62. const cc = Logger.getCorrelationContext();
  63. const mapping = mappings[mappings.length - 1];
  64. if (mapping !== this._mapping || mapping[key] == null) {
  65. if (cc != null) {
  66. cc.exitDetails = ' \u2022 skipped';
  67. }
  68. return;
  69. }
  70. mapping[key] = undefined;
  71. await setContext(`${ContextKeys.Key}:${key}`, false);
  72. }
  73. @log({
  74. args: false,
  75. prefix: context => `${context.prefix}(paused=${context.instance._paused})`,
  76. })
  77. async pause(keys?: Keys[]) {
  78. if (this._paused) return;
  79. this._paused = true;
  80. const mapping = (Object.keys(this._mapping) as Keys[]).reduce((accumulator, key) => {
  81. accumulator[key] = keys == null || keys.includes(key) ? undefined : this._mapping[key];
  82. return accumulator;
  83. }, Object.create(null) as KeyMapping);
  84. await this.updateKeyCommandsContext(mapping);
  85. }
  86. @log({
  87. args: false,
  88. prefix: context => `${context.prefix}(paused=${context.instance._paused})`,
  89. })
  90. async resume() {
  91. if (!this._paused) return;
  92. this._paused = false;
  93. await this.updateKeyCommandsContext(this._mapping);
  94. }
  95. async start() {
  96. await this.resume();
  97. }
  98. @log<KeyboardScope['setKeyCommand']>({
  99. args: false,
  100. prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`,
  101. })
  102. async setKeyCommand(key: Keys, command: KeyCommand | (() => Promise<KeyCommand>)) {
  103. const cc = Logger.getCorrelationContext();
  104. const mapping = mappings[mappings.length - 1];
  105. if (mapping !== this._mapping) {
  106. if (cc != null) {
  107. cc.exitDetails = ' \u2022 skipped';
  108. }
  109. return;
  110. }
  111. const set = Boolean(mapping[key]);
  112. mapping[key] = command;
  113. if (!set) {
  114. await setContext(`${ContextKeys.Key}:${key}`, true);
  115. }
  116. }
  117. private async updateKeyCommandsContext(mapping: KeyMapping) {
  118. await Promise.all(keys.map(key => setContext(`${ContextKeys.Key}:${key}`, Boolean(mapping?.[key]))));
  119. }
  120. }
  121. export class Keyboard implements Disposable {
  122. private readonly _disposable: Disposable;
  123. constructor() {
  124. const subscriptions = keys.map(key =>
  125. commands.registerCommand(`gitlens.key.${key}`, () => this.execute(key), this),
  126. );
  127. this._disposable = Disposable.from(...subscriptions);
  128. }
  129. dispose() {
  130. this._disposable.dispose();
  131. }
  132. @log<Keyboard['createScope']>({
  133. args: false,
  134. prefix: (context, mapping) =>
  135. `${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`,
  136. })
  137. createScope(mapping?: KeyMapping): KeyboardScope {
  138. return new KeyboardScope({ ...mapping });
  139. }
  140. @log<Keyboard['beginScope']>({
  141. args: false,
  142. prefix: (context, mapping) =>
  143. `${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`,
  144. })
  145. async beginScope(mapping?: KeyMapping): Promise<KeyboardScope> {
  146. const scope = this.createScope(mapping);
  147. await scope.start();
  148. return scope;
  149. }
  150. @log()
  151. async execute(key: Keys): Promise<void> {
  152. const cc = Logger.getCorrelationContext();
  153. if (!mappings.length) {
  154. if (cc != null) {
  155. cc.exitDetails = ' \u2022 skipped, no mappings';
  156. }
  157. return;
  158. }
  159. try {
  160. const mapping = mappings[mappings.length - 1];
  161. let command = mapping[key] as KeyCommand | (() => Promise<KeyCommand>);
  162. if (typeof command === 'function') {
  163. command = await command();
  164. }
  165. if (typeof command?.onDidPressKey !== 'function') {
  166. if (cc != null) {
  167. cc.exitDetails = ' \u2022 skipped, no callback';
  168. }
  169. return;
  170. }
  171. void (await command.onDidPressKey(key));
  172. } catch (ex) {
  173. Logger.error(ex, cc);
  174. }
  175. }
  176. }